github.com/decred/politeia@v1.4.0/politeiad/cmd/legacypoliteia/cmd_import.go (about)

     1  // Copyright (c) 2022 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/binary"
    11  	"encoding/hex"
    12  	"encoding/json"
    13  	"errors"
    14  	"flag"
    15  	"fmt"
    16  	"io/fs"
    17  	"net/http"
    18  	"os"
    19  	"path/filepath"
    20  	"sort"
    21  	"strings"
    22  	"sync"
    23  	"time"
    24  
    25  	"github.com/decred/dcrd/chaincfg/v3"
    26  	"github.com/decred/dcrd/dcrutil/v3"
    27  	"github.com/decred/politeia/politeiad/api/v1/identity"
    28  	"github.com/decred/politeia/politeiad/api/v1/mime"
    29  	backend "github.com/decred/politeia/politeiad/backendv2"
    30  	"github.com/decred/politeia/politeiad/backendv2/tstorebe/store"
    31  	"github.com/decred/politeia/politeiad/backendv2/tstorebe/store/mysql"
    32  	"github.com/decred/politeia/politeiad/backendv2/tstorebe/tlog"
    33  	"github.com/decred/politeia/politeiad/backendv2/tstorebe/tstore"
    34  	"github.com/decred/politeia/politeiad/plugins/comments"
    35  	"github.com/decred/politeia/politeiad/plugins/pi"
    36  	"github.com/decred/politeia/politeiad/plugins/ticketvote"
    37  	"github.com/decred/politeia/politeiad/plugins/usermd"
    38  	"github.com/decred/politeia/politeiawww/config"
    39  	"github.com/decred/politeia/politeiawww/legacy/user"
    40  	userdb "github.com/decred/politeia/politeiawww/legacy/user/mysql"
    41  	"github.com/decred/politeia/util"
    42  	"github.com/google/trillian"
    43  	"github.com/google/uuid"
    44  	"google.golang.org/grpc/codes"
    45  )
    46  
    47  const (
    48  	// tstore settings
    49  	defaultTlogHost = "localhost:8090"
    50  	defaultDBHost   = "localhost:3306"
    51  	defaultDBPass   = "politeiadpass"
    52  
    53  	// User database settings
    54  	userDBPass = "politeiawwwpass"
    55  )
    56  
    57  var (
    58  	// CLI flags for the import command. We print a custom usage message,
    59  	// see usage.go, so the individual flag usage messages are left blank.
    60  	importFlags = flag.NewFlagSet(importCmdName, flag.ExitOnError)
    61  	testnet     = importFlags.Bool("testnet", false, "")
    62  	tlogHost    = importFlags.String("tloghost", defaultTlogHost, "")
    63  	dbHost      = importFlags.String("dbhost", defaultDBHost, "")
    64  	dbPass      = importFlags.String("dbpass", defaultDBPass, "")
    65  	importToken = importFlags.String("token", "", "")
    66  	stubUsers   = importFlags.Bool("stubusers", false, "")
    67  
    68  	// tstore settings
    69  	politeiadHomeDir = dcrutil.AppDataDir("politeiad", false)
    70  	politeiadDataDir = filepath.Join(politeiadHomeDir, "data")
    71  	// dcrtimeHost      = "" // Not needed for import
    72  	// dcrtimeCert      = "" // Not needed for import
    73  
    74  	// User database settings
    75  	userDBEncryptionKey = filepath.Join(config.DefaultHomeDir, "sbox.key")
    76  )
    77  
    78  // execImportCmd executes the import command.
    79  func execImportCmd(args []string) error {
    80  	// Verify the legacy directory exists
    81  	if len(args) == 0 {
    82  		return fmt.Errorf("legacy dir argument not provided")
    83  	}
    84  	legacyDir := util.CleanAndExpandPath(args[0])
    85  	if _, err := os.Stat(legacyDir); err != nil {
    86  		return fmt.Errorf("legacy directory not found: %v", legacyDir)
    87  	}
    88  
    89  	// Parse the CLI flags
    90  	err := importFlags.Parse(args[1:])
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	// Testnet or mainnet
    96  	params := config.MainNetParams.Params
    97  	if *testnet {
    98  		params = config.TestNet3Params.Params
    99  	}
   100  
   101  	fmt.Printf("\n")
   102  	fmt.Printf("Command parameters\n")
   103  	fmt.Printf("Network  : %v\n", params.Name)
   104  	fmt.Printf("Tlog host: %v\n", *tlogHost)
   105  	fmt.Printf("DB host  : %v\n", *dbHost)
   106  	fmt.Printf("\n")
   107  
   108  	// Print the total elapsed time on exit
   109  	t := time.Now()
   110  	defer func() {
   111  		fmt.Printf("Import elapsed time: %v\n", time.Since(t))
   112  	}()
   113  
   114  	// Setup the import command context
   115  	c, err := newImportCmd(legacyDir, *tlogHost, *dbHost, *dbPass,
   116  		*importToken, *stubUsers, params)
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	// Import the legacy proposals
   122  	return c.importLegacyProposals()
   123  }
   124  
   125  // importCmd implements the legacypoliteia import command. The import command
   126  // reads the output of the convert command from disk and imports it into the
   127  // politeiad tstore backend.
   128  //
   129  // The performance bottleneck for this command is the trillian log server (tlog
   130  // server). ~50 leaves/sec can be appended onto a tlog tree. This means that
   131  // importing 10,000 proposal votes will take ~200 seconds (3 minutes, 20
   132  // seconds). The vast majority of the execution time of this command is spent
   133  // importing proposal votes.
   134  //
   135  // The command is relatively light weight. It's memory footprint should stay
   136  // under 100 MiB and CPU usage should be minimal.
   137  type importCmd struct {
   138  	sync.Mutex
   139  	legacyDir string
   140  	tlogHost  string
   141  	token     string // Optional
   142  	stubUsers bool
   143  	tstore    *tstore.Tstore
   144  
   145  	// The following are used to import the proposal votes into tstore manually
   146  	// in order to increase performance to an acceptable speed.
   147  	kv         store.BlobKV
   148  	tlogClient tlog.Client
   149  
   150  	// The following fields will only be populated when the caller provides
   151  	// the stub users flag.
   152  	userDB user.Database
   153  	http   *http.Client
   154  }
   155  
   156  // newImportCmd returns a new importCmd.
   157  func newImportCmd(legacyDir, tlogHost, dbHost, dbPass, importToken string, stubUsers bool, params *chaincfg.Params) (*importCmd, error) {
   158  	// Setup the tstore connection
   159  	ts, err := tstore.New(politeiadHomeDir, politeiadDataDir,
   160  		params, tlogHost, dbHost, dbPass, "", "")
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  
   165  	// Setup key-value store
   166  	var (
   167  		dbUser = "politeiad"
   168  		dbName = fmt.Sprintf("%v_kv", params.Name)
   169  	)
   170  	kv, err := mysql.New(dbHost, dbUser, dbPass, dbName)
   171  	if err != nil {
   172  		return nil, err
   173  
   174  	}
   175  
   176  	// Setup trillian client
   177  	tlogClient, err := tlog.NewClient(tlogHost)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  
   182  	// Setup the user database connection
   183  	var (
   184  		userDB user.Database
   185  		httpC  *http.Client
   186  	)
   187  	if stubUsers {
   188  		userDB, err = userdb.New(dbHost, userDBPass,
   189  			params.Name, userDBEncryptionKey)
   190  		if err != nil {
   191  			return nil, err
   192  		}
   193  		httpC, err = util.NewHTTPClient(false, "")
   194  		if err != nil {
   195  			return nil, err
   196  		}
   197  	}
   198  
   199  	return &importCmd{
   200  		legacyDir:  legacyDir,
   201  		token:      importToken,
   202  		tlogHost:   tlogHost,
   203  		stubUsers:  stubUsers,
   204  		tstore:     ts,
   205  		kv:         kv,
   206  		tlogClient: tlogClient,
   207  		userDB:     userDB,
   208  		http:       httpC,
   209  	}, nil
   210  }
   211  
   212  // importProposals walks the legacy directory and imports the legacy proposals
   213  // into tstore. It accomplishes this using the following steps:
   214  //
   215  // 1. Inventory all legacy proposals being imported.
   216  //
   217  // 2. Retrieve the tstore token inventory.
   218  //
   219  //  3. Iterate through each record in the existing tstore inventory and check
   220  //     if the record corresponds to one of the legacy proposals.
   221  //
   222  //  4. Perform an fsck on all legacy proposals that already exist in tstore to
   223  //     verify that the full legacy proposal has been imported. Any missing
   224  //     legacy proposal content is added to tstore during this step. A partial
   225  //     import can happen if the import command was being run and was stopped
   226  //     prior to completion or if it encountered an unexpected error.
   227  //
   228  //  5. Add the legacy RFP proposals to tstore. This must be done first so that
   229  //     the RFP submissions can link to the tstore RFP proposal token.
   230  //
   231  // 6. Add the remaining legacy proposals to tstore.
   232  //
   233  //  7. Add a startRunoffRecord for each RFP proposal vote. The record is added
   234  //     to the RFP parent's tlog tree. This is required in order to mimic what
   235  //     would happen under normal operating conditions.
   236  func (c *importCmd) importLegacyProposals() error {
   237  	// 1. Inventory all legacy proposals being imported
   238  	legacyInv, err := parseLegacyTokens(c.legacyDir)
   239  	if err != nil {
   240  		return err
   241  	}
   242  	legacyInvM := make(map[string]struct{}, len(legacyInv))
   243  	for _, token := range legacyInv {
   244  		legacyInvM[token] = struct{}{}
   245  	}
   246  
   247  	fmt.Printf("%v legacy proposals found for import\n", len(legacyInv))
   248  
   249  	// 2. Retrieve the tstore token inventory
   250  	inv, err := c.tstore.Inventory()
   251  	if err != nil {
   252  		return err
   253  	}
   254  
   255  	fmt.Printf("%v existing proposals found in tstore\n", len(inv))
   256  
   257  	// imported contains the legacy tokens of all legacy proposals
   258  	// that have already been imported into tstore. This list does
   259  	// not differentiate between partially imported or fully
   260  	// imported proposals. The fsck function checks for and handles
   261  	// partially imported proposals.
   262  	//
   263  	// map[legacyToken]tstoreToken
   264  	imported := make(map[string][]byte, len(legacyInv))
   265  
   266  	// startRunoffRecords is used to aggregate the data for runoff
   267  	// votes. This is done during runtime because the tstore tokens
   268  	// for all of the RFP submissions must be compiled before the
   269  	// startRunoffRecord can be saved to the parent RFP tree.
   270  	//
   271  	// map[tstoreTokenForParentRFP]startRunoffRecord
   272  	startRunoffRecords := make(map[string]startRunoffRecord, len(legacyInv))
   273  
   274  	// 3. Iterate through each record in the existing tstore
   275  	// inventory and check if the record corresponds to one
   276  	// of the legacy proposals.
   277  	for _, tstoreToken := range inv {
   278  		// Get the record metadata from tstore
   279  		filenames := []string{pi.FileNameProposalMetadata}
   280  		r, err := c.tstore.RecordPartial(tstoreToken, 0, filenames, false)
   281  		if err != nil {
   282  			return err
   283  		}
   284  		switch r.RecordMetadata.Status {
   285  		case backend.StatusPublic, backend.StatusArchived:
   286  			// These statuses are expected
   287  		default:
   288  			// This is not a record that we're interested in.
   289  			// The legacy proposals are all going to be either
   290  			// public or archived.
   291  			continue
   292  		}
   293  
   294  		// Check if this is a legacy proposal
   295  		pm, err := decodeProposalMetadata(r.Files)
   296  		if err != nil {
   297  			return err
   298  		}
   299  		if pm.LegacyToken == "" {
   300  			// This is not a legacy proposal
   301  			continue
   302  		}
   303  
   304  		// This is a legacy proposal. Add it to the imported list.
   305  		imported[pm.LegacyToken] = tstoreToken
   306  	}
   307  
   308  	fmt.Printf("%v legacy proposals were found in tstore\n", len(imported))
   309  
   310  	// 4. Perform an fsck on all legacy proposals that already exist
   311  	//    in tstore to verify that the full legacy proposal has been
   312  	//    imported. Any missing legacy proposal content is added to
   313  	//    tstore during this step. A partial import can happen if
   314  	//    the import command was being run and was stopped prior to
   315  	//    completion or if it encountered an unexpected error.
   316  	for legacyToken, tstoreToken := range imported {
   317  		err := c.fsckProposal(legacyToken, tstoreToken)
   318  		if err != nil {
   319  			return err
   320  		}
   321  	}
   322  
   323  	// 5. Add the legacy RFP proposals to tstore. This must be done
   324  	//    first so that the RFP submissions can link to the tstore
   325  	//    RFP proposal token.
   326  	for _, legacyToken := range legacyInv {
   327  		if c.token != "" && c.token != legacyToken {
   328  			// The caller wants to import a specific
   329  			// proposal and this is not it.
   330  			continue
   331  		}
   332  		if _, ok := imported[legacyToken]; ok {
   333  			// This proposal has already been imported
   334  			continue
   335  		}
   336  		p, err := readProposal(c.legacyDir, legacyToken)
   337  		if err != nil {
   338  			return err
   339  		}
   340  		if !p.isRFP() {
   341  			// This is not an RFP. Skip it for now.
   342  			continue
   343  		}
   344  
   345  		fmt.Printf("Importing proposal %v/%v\n", len(imported)+1, len(legacyInv))
   346  
   347  		tstoreToken, err := c.importProposal(p, nil)
   348  		if err != nil {
   349  			return err
   350  		}
   351  
   352  		imported[legacyToken] = tstoreToken
   353  	}
   354  
   355  	// 6. Add the remaining legacy proposals to tstore
   356  	for _, legacyToken := range legacyInv {
   357  		if c.token != "" && c.token != legacyToken {
   358  			// The caller wants to import a specific
   359  			// proposal and this is not it.
   360  			continue
   361  		}
   362  		if _, ok := imported[legacyToken]; ok {
   363  			// This proposal has already been imported
   364  			continue
   365  		}
   366  
   367  		fmt.Printf("Importing proposal %v/%v\n", len(imported)+1, len(legacyInv))
   368  
   369  		// Read the proposal from disk
   370  		p, err := readProposal(c.legacyDir, legacyToken)
   371  		if err != nil {
   372  			return err
   373  		}
   374  
   375  		// Lookup th RFP parent tstore token if this is an RFP submission.
   376  		// The RFP submissions must reference the parent RFP tstore token,
   377  		// not the parent RFP legacy token.
   378  		var parentTstoreToken []byte
   379  		if p.isRFPSubmission() {
   380  			parentTstoreToken = imported[p.VoteMetadata.LinkTo]
   381  			if parentTstoreToken == nil {
   382  				// Should not happen
   383  				return fmt.Errorf("rpf parent tstore token not found")
   384  			}
   385  		}
   386  
   387  		// Import the proposal
   388  		tstoreToken, err := c.importProposal(p, parentTstoreToken)
   389  		if err != nil {
   390  			return err
   391  		}
   392  
   393  		imported[legacyToken] = tstoreToken
   394  
   395  		// Aggregate the runoff vote data needed for the startRunoffRecord.
   396  		// This is only necessary if this proposal in an RFP submission.
   397  		if parentTstoreToken != nil {
   398  			parentToken := hex.EncodeToString(parentTstoreToken)
   399  			srr, ok := startRunoffRecords[parentToken]
   400  			if !ok {
   401  				srr = startRunoffRecord{
   402  					Submissions:      []string{},
   403  					Mask:             p.VoteDetails.Params.Mask,
   404  					Duration:         p.VoteDetails.Params.Duration,
   405  					QuorumPercentage: p.VoteDetails.Params.QuorumPercentage,
   406  					PassPercentage:   p.VoteDetails.Params.PassPercentage,
   407  					StartBlockHeight: p.VoteDetails.StartBlockHeight,
   408  					StartBlockHash:   p.VoteDetails.StartBlockHash,
   409  					EndBlockHeight:   p.VoteDetails.EndBlockHeight,
   410  					EligibleTickets:  p.VoteDetails.EligibleTickets,
   411  				}
   412  			}
   413  
   414  			submissionToken := hex.EncodeToString(tstoreToken)
   415  			srr.Submissions = append(srr.Submissions, submissionToken)
   416  
   417  			startRunoffRecords[parentToken] = srr
   418  		}
   419  	}
   420  
   421  	// 7. Add a startRunoffRecord for each RFP proposal vote. The
   422  	//    record is added to the RFP parent's tlog tree. This is
   423  	//    required in order to mimic what would happen under normal
   424  	//    operating conditions.
   425  	for parentTstoreToken, srr := range startRunoffRecords {
   426  		fmt.Printf("Importing start runoff record to %v\n", parentTstoreToken)
   427  
   428  		parent, err := hex.DecodeString(parentTstoreToken)
   429  		if err != nil {
   430  			return err
   431  		}
   432  		err = c.saveStartRunoffRecord(parent, srr)
   433  		if err != nil {
   434  			return err
   435  		}
   436  	}
   437  
   438  	return nil
   439  }
   440  
   441  // fsckProposal verifies that a legacy proposal has been fully imported into
   442  // tstore. If a partial import is found, this function will pick up where the
   443  // previous invocation left off and finish the import.
   444  func (c *importCmd) fsckProposal(legacyToken string, tstoreToken []byte) error {
   445  	fmt.Printf("Fsck proposal %x %v\n", tstoreToken, legacyToken)
   446  
   447  	// This is non-trivial to implement and will only be needed
   448  	// if an error occurs during the import process. We'll leave
   449  	// this unimplemented for now and only implement it if
   450  	// something goes wrong during the production import process
   451  	// and we actually need it.
   452  
   453  	return nil
   454  }
   455  
   456  // importProposal imports the specified legacy proposal into tstore and returns
   457  // the tstore token that is created during import.
   458  //
   459  // parentTstoreToken is an optional argument that will be populated for RFP
   460  // submissions. The parentTstoreToken is the parent RFP tstore token that the
   461  // RFP submissions will need to reference. This argument will be nil for all
   462  // proposals that are not RFP submissions.
   463  //
   464  // This function assumes that the proposal does not yet exist in tstore.
   465  // Handling proposals that have been partially added is done by the
   466  // fsckProposal function.
   467  func (c *importCmd) importProposal(p *proposal, parentTstoreToken []byte) ([]byte, error) {
   468  	fmt.Printf("  Legacy token: %v\n", p.RecordMetadata.Token)
   469  
   470  	// Create a new tstore record entry
   471  	tstoreToken, err := c.tstore.RecordNew()
   472  	if err != nil {
   473  		return nil, err
   474  	}
   475  
   476  	fmt.Printf("  Tstore token: %x\n", tstoreToken)
   477  
   478  	// Perform proposal data changes
   479  	err = overwriteProposalFields(p, tstoreToken, parentTstoreToken)
   480  	if err != nil {
   481  		return nil, err
   482  	}
   483  
   484  	// Import the proposal contents
   485  	fmt.Printf("  Importing record data...\n")
   486  	err = c.importRecord(*p, tstoreToken)
   487  	if err != nil {
   488  		return nil, err
   489  	}
   490  
   491  	fmt.Printf("  Importing comment plugin data...\n")
   492  	err = c.importCommentPluginData(*p, tstoreToken)
   493  	if err != nil {
   494  		return nil, err
   495  	}
   496  
   497  	fmt.Printf("  Importing ticketvote plugin data...\n")
   498  	err = c.importTicketvotePluginData(*p, tstoreToken)
   499  	if err != nil {
   500  		return nil, err
   501  	}
   502  
   503  	// Stub the user in the politeiawww user database
   504  	if c.stubUsers {
   505  		err := c.stubProposalUsers(*p)
   506  		if err != nil {
   507  			return nil, err
   508  		}
   509  	}
   510  
   511  	return tstoreToken, nil
   512  }
   513  
   514  // importRecord imports the backend record portion of a proposal into tstore
   515  // using the same steps that would occur under if the proposal was saved under
   516  // normal conditions and not being imported by this tool. This is required
   517  // because there are certain steps that the tstore backend must complete, ex.
   518  // re-saving encrypted blobs as plain text when a proposal is made public, in
   519  // order for the proposal to be imported correctly.
   520  func (c *importCmd) importRecord(p proposal, tstoreToken []byte) error {
   521  	// Convert user generated metadata into backend files.
   522  	//
   523  	// User generated metadata includes:
   524  	// - pi plugin ProposalMetadata
   525  	// - ticketvote plugin VoteMetadata (may not exist)
   526  	f, err := convertProposalMetadataToFile(p.ProposalMetadata)
   527  	if err != nil {
   528  		return err
   529  	}
   530  	p.Files = append(p.Files, *f)
   531  
   532  	if p.VoteMetadata != nil {
   533  		f, err := convertVoteMetadataToFile(*p.VoteMetadata)
   534  		if err != nil {
   535  			return err
   536  		}
   537  		p.Files = append(p.Files, *f)
   538  	}
   539  
   540  	// Convert server generated metadata into backed metadata streams.
   541  	//
   542  	// Server generated metadata includes:
   543  	// - user plugin StatusChangeMetadata
   544  	// - user plugin UserMetadata
   545  	//
   546  	// Public proposals will only have one status change. Abandoned
   547  	// proposals will have two status changes, the public status change
   548  	// and the archived status change. The status changes are handled
   549  	// individually and not automatically added to the same metadata
   550  	// stream so that we can mimick how status change data is saved
   551  	// under normal operation.
   552  	userStream, err := convertUserMetadataToMetadataStream(p.UserMetadata)
   553  	if err != nil {
   554  		return err
   555  	}
   556  
   557  	var (
   558  		publicStatus    = p.StatusChanges[0]
   559  		abandonedStatus *usermd.StatusChangeMetadata
   560  	)
   561  	if len(p.StatusChanges) > 1 {
   562  		abandonedStatus = &p.StatusChanges[1]
   563  	}
   564  
   565  	// Cache the record status that we will end up at. We
   566  	// must go through the normal status iterations in order
   567  	// to import the proposal correctly.
   568  	//
   569  	// Ex: unreviewed -> public -> abandoned
   570  	status := p.RecordMetadata.Status
   571  
   572  	// Save the proposal as unvetted
   573  	p.RecordMetadata.State = backend.StateUnvetted
   574  	p.RecordMetadata.Status = backend.StatusUnreviewed
   575  
   576  	metadataStreams := []backend.MetadataStream{
   577  		*userStream,
   578  	}
   579  
   580  	err = c.tstore.RecordSave(tstoreToken, p.RecordMetadata,
   581  		metadataStreams, p.Files)
   582  	if err != nil {
   583  		return err
   584  	}
   585  
   586  	// Save the proposal as vetted. The public status change
   587  	// is added to the status change metadata stream during
   588  	// this step.  The timestamp is incremented by 1 second
   589  	// so it's not the same timestamp as the unvetted version.
   590  	p.RecordMetadata.State = backend.StateVetted
   591  	p.RecordMetadata.Status = backend.StatusPublic
   592  	p.RecordMetadata.Timestamp += 1
   593  
   594  	statusChangeStream, err := convertStatusChangeToMetadataStream(publicStatus)
   595  	if err != nil {
   596  		return err
   597  	}
   598  
   599  	metadataStreams = []backend.MetadataStream{
   600  		*userStream,
   601  		*statusChangeStream,
   602  	}
   603  
   604  	err = c.tstore.RecordSave(tstoreToken, p.RecordMetadata,
   605  		metadataStreams, p.Files)
   606  	if err != nil {
   607  		return err
   608  	}
   609  
   610  	switch status {
   611  	case backend.StatusPublic:
   612  		// This is a public proposal. There is nothing else
   613  		// that needs to be done.
   614  		return nil
   615  
   616  	case backend.StatusArchived:
   617  		// This is an abandoned proposal. Continue so that the
   618  		// status is updated below.
   619  
   620  	default:
   621  		// This should not happen. There should only be public
   622  		// and abandoned proposals.
   623  		return fmt.Errorf("invalid record status %v", status)
   624  	}
   625  
   626  	// This is an abandoned proposal. Update the record metadata,
   627  	// add the abandoned status to the status changes metadata
   628  	// stream, and freeze the tstore record. This is what would
   629  	// happen under regular operating conditions. The timestamp
   630  	// is incremented by 1 second so that it is unique.
   631  	p.RecordMetadata.Status = backend.StatusArchived
   632  	p.RecordMetadata.Iteration += 1
   633  	p.RecordMetadata.Timestamp += 1
   634  
   635  	abandonedStream, err := convertStatusChangeToMetadataStream(*abandonedStatus)
   636  	if err != nil {
   637  		return err
   638  	}
   639  
   640  	metadataStreams = []backend.MetadataStream{
   641  		*userStream,
   642  		appendMetadataStream(*statusChangeStream, *abandonedStream),
   643  	}
   644  
   645  	return c.tstore.RecordFreeze(tstoreToken, p.RecordMetadata,
   646  		metadataStreams, p.Files)
   647  }
   648  
   649  // importCommentPluginData imports the comment plugin data into tstore for
   650  // the provided proposal.
   651  func (c *importCmd) importCommentPluginData(p proposal, tstoreToken []byte) error {
   652  	for i, v := range p.CommentAdds {
   653  		s := fmt.Sprintf("    Comment add %v/%v", i+1, len(p.CommentAdds))
   654  		printInPlace(s)
   655  
   656  		err := c.saveCommentAdd(tstoreToken, v)
   657  		if err != nil {
   658  			return err
   659  		}
   660  
   661  		if i == len(p.CommentAdds)-1 {
   662  			fmt.Printf("\n")
   663  		}
   664  	}
   665  	for i, v := range p.CommentDels {
   666  		s := fmt.Sprintf("    Comment del %v/%v", i+1, len(p.CommentDels))
   667  		printInPlace(s)
   668  
   669  		err := c.saveCommentDel(tstoreToken, v)
   670  		if err != nil {
   671  			return err
   672  		}
   673  
   674  		if i == len(p.CommentDels)-1 {
   675  			fmt.Printf("\n")
   676  		}
   677  	}
   678  	for i, v := range p.CommentVotes {
   679  		s := fmt.Sprintf("    Comment vote %v/%v", i+1, len(p.CommentVotes))
   680  		printInPlace(s)
   681  
   682  		err := c.saveCommentVote(tstoreToken, v)
   683  		if err != nil {
   684  			return err
   685  		}
   686  
   687  		if i == len(p.CommentVotes)-1 {
   688  			fmt.Printf("\n")
   689  		}
   690  	}
   691  	return nil
   692  }
   693  
   694  // importTicketvotePluginData imports the ticketvote plugin data into tstore
   695  // for the provided proposal.
   696  //
   697  // Some proposals we're never voted on and therefor do not have any ticketvote
   698  // plugin data that needs to be imported.
   699  func (c *importCmd) importTicketvotePluginData(p proposal, tstoreToken []byte) error {
   700  	// Save the auth details
   701  	if p.AuthDetails == nil {
   702  		return nil
   703  	}
   704  
   705  	fmt.Printf("    Auth details\n")
   706  
   707  	err := c.saveAuthDetails(tstoreToken, *p.AuthDetails)
   708  	if err != nil {
   709  		return err
   710  	}
   711  
   712  	// Save the vote details
   713  	if p.VoteDetails == nil {
   714  		return nil
   715  	}
   716  
   717  	fmt.Printf("    Vote details\n")
   718  
   719  	err = c.saveVoteDetails(tstoreToken, *p.VoteDetails)
   720  	if err != nil {
   721  		return err
   722  	}
   723  
   724  	// Save the cast votes. These are saved concurrently in batches
   725  	// to get around the tlog signer performance bottleneck. The tlog
   726  	// signer will only append queued leaves onto a tlog tree every
   727  	// xxx interval, where xxx is a config setting that is currently
   728  	// configured to 200ms for politeia. If we did not submit the
   729  	// votes concurrently, each vote would take at least 200ms to
   730  	// be appended, which is unacceptably slow when you have tens of
   731  	// thousands of votes to import.
   732  	//
   733  	// tlog is incredibly finicky. I think there is a deadlock bug
   734  	// somewhere in the trillian log server that gets hit when a large
   735  	// number of leaves are being appended. A batch size of 50 was
   736  	// found during testing to be a good balance between performance
   737  	// and errors. Increasing the batch size speeds up the importing,
   738  	// but also results in more deadlocks.
   739  	var (
   740  		batchSize = 50
   741  		startIdx  = 0
   742  
   743  		t = time.Now()
   744  	)
   745  	for startIdx < len(p.CastVotes) {
   746  		endIdx := startIdx + batchSize
   747  		if endIdx > len(p.CastVotes) {
   748  			endIdx = len(p.CastVotes)
   749  		}
   750  
   751  		s := fmt.Sprintf("    Cast vote %v/%v", endIdx, len(p.CastVotes))
   752  		printInPlace(s)
   753  
   754  		c.saveVoteBatch(tstoreToken, p.CastVotes[startIdx:endIdx])
   755  
   756  		startIdx += batchSize
   757  	}
   758  	fmt.Printf("\n")
   759  
   760  	fmt.Printf("    Elapsed vote import time: %v\n", time.Since(t))
   761  
   762  	return nil
   763  }
   764  
   765  // SavePluginBlobEntry is a light weight version of the TstoreClient BlobSave
   766  // method that is used during normal operation of politeiad when saving plugin
   767  // data. This light weight function is necessary to increase performance of
   768  // a plugin data blob to an acceptable speed for this command.
   769  func (c *importCmd) savePluginBlobEntry(token []byte, be store.BlobEntry) error {
   770  	// Prepare key-value store blob
   771  	digest, err := hex.DecodeString(be.Digest)
   772  	if err != nil {
   773  		return err
   774  	}
   775  	blob, err := store.Blobify(be)
   776  	if err != nil {
   777  		return err
   778  	}
   779  	key := uuid.New().String()
   780  	kv := map[string][]byte{key: blob}
   781  
   782  	// Save the blob to store
   783  	err = c.kv.Put(kv, false)
   784  	if err != nil {
   785  		return err
   786  	}
   787  
   788  	// Setup the tlog leaf extra data
   789  	type extraData struct {
   790  		Key   string         `json:"k"`
   791  		Desc  string         `json:"d"`
   792  		State backend.StateT `json:"s,omitempty"`
   793  	}
   794  	b, err := base64.StdEncoding.DecodeString(be.DataHint)
   795  	if err != nil {
   796  		return err
   797  	}
   798  	var dd store.DataDescriptor
   799  	err = json.Unmarshal(b, &dd)
   800  	if err != nil {
   801  		return err
   802  	}
   803  	ed := extraData{
   804  		Key:   key,
   805  		Desc:  dd.Descriptor,
   806  		State: backend.StateVetted,
   807  	}
   808  	extraDataB, err := json.Marshal(ed)
   809  	if err != nil {
   810  		return err
   811  	}
   812  
   813  	// Append log leaf to trillian tree
   814  	var (
   815  		treeID = int64(binary.LittleEndian.Uint64(token))
   816  		leaves = []*trillian.LogLeaf{
   817  			tlog.NewLogLeaf(digest, extraDataB),
   818  		}
   819  	)
   820  	queued, _, err := c.tlogClient.LeavesAppend(treeID, leaves)
   821  	if err != nil {
   822  		return err
   823  	}
   824  	if len(queued) != 1 {
   825  		return fmt.Errorf("got %v queued leaves, want 1", len(queued))
   826  	}
   827  	code := codes.Code(queued[0].QueuedLeaf.GetStatus().GetCode())
   828  	switch code {
   829  	case codes.OK:
   830  		// This is ok; continue
   831  	case codes.AlreadyExists:
   832  		return backend.ErrDuplicatePayload
   833  	default:
   834  		return fmt.Errorf("queued leaf error: %v", c)
   835  	}
   836  
   837  	return nil
   838  }
   839  
   840  // saveVoteBatch saves a batch of cast votes to tstore. This includes appending
   841  // leaves onto the tlog tree and saving the data blobs to the key-value store.
   842  //
   843  // tlog is incredibly finicky. I think there is a deadlock bug somewhere in the
   844  // trillian log server that gets hit when a large number of leaves are being
   845  // appended. The tlog server will periodically freeze up without throwing any
   846  // errors and will require a hard restart. This function was written in a way
   847  // that mitigates this issue as much as possible. If the trillian log server
   848  // freezes up, this function will be stuck in a rety loop until the trillian
   849  // lop server is reset.
   850  func (c *importCmd) saveVoteBatch(tstoreToken []byte, votes []ticketvote.CastVoteDetails) {
   851  	var wg sync.WaitGroup
   852  	for _, v := range votes {
   853  		// Increment the wait group
   854  		wg.Add(1)
   855  
   856  		go func(cvd ticketvote.CastVoteDetails) {
   857  			// Decrement the wait group on successful completion
   858  			defer func() {
   859  				wg.Done()
   860  			}()
   861  
   862  			var voteSaved bool
   863  			for !voteSaved {
   864  				err := c.saveCastVoteDetails(tstoreToken, cvd)
   865  				switch {
   866  				case err == nil:
   867  					voteSaved = true
   868  
   869  				case strings.Contains(err.Error(), "duplicate payload"):
   870  					fmt.Printf("\n")
   871  					fmt.Printf("%v: %v\n", cvd.Ticket, err)
   872  					fmt.Printf("Vote %v already saved; skipping\n", cvd.Ticket)
   873  
   874  					voteSaved = true
   875  
   876  				default:
   877  					fmt.Printf("\n")
   878  					fmt.Printf("Failed to save cast vote %v: %v\n", cvd.Ticket, err)
   879  					fmt.Printf("Retrying cast vote %v\n", cvd.Ticket)
   880  					time.Sleep(50 * time.Millisecond)
   881  				}
   882  			}
   883  
   884  			// Not exactly sure why, but this reduces the number of failed
   885  			// tlog appends.
   886  			time.Sleep(50 * time.Millisecond)
   887  
   888  			var colliderSaved bool
   889  			for !colliderSaved {
   890  				vc := voteCollider{
   891  					Token:  cvd.Token,
   892  					Ticket: cvd.Ticket,
   893  				}
   894  				err := c.saveVoteCollider(tstoreToken, vc)
   895  				switch {
   896  				case err == nil:
   897  					colliderSaved = true
   898  
   899  				case strings.Contains(err.Error(), "duplicate payload"):
   900  					fmt.Printf("\n")
   901  					fmt.Printf("%v: %v\n", cvd.Ticket, err)
   902  					fmt.Printf("Vote collider %v already saved; skipping\n", cvd.Ticket)
   903  
   904  					colliderSaved = true
   905  
   906  				default:
   907  					fmt.Printf("\n")
   908  					fmt.Printf("Failed to save vote collider %v: %v\n", cvd.Ticket, err)
   909  					fmt.Printf("Retrying vote collider %v\n", cvd.Ticket)
   910  					time.Sleep(50 * time.Millisecond)
   911  				}
   912  			}
   913  		}(v)
   914  	}
   915  
   916  	// Wait for all votes to be successfully saved
   917  	wg.Wait()
   918  }
   919  
   920  const (
   921  	// The following data descriptors were pulled from the plugins. They're not
   922  	// exported from the plugins and under normal circumstances there's no reason
   923  	// to have them as exported variables, so we duplicate them here.
   924  
   925  	// comments plugin data descriptors
   926  	dataDescriptorCommentAdd  = comments.PluginID + "-add-v1"
   927  	dataDescriptorCommentDel  = comments.PluginID + "-del-v1"
   928  	dataDescriptorCommentVote = comments.PluginID + "-vote-v1"
   929  
   930  	// ticketvote plugin data descriptors
   931  	dataDescriptorAuthDetails     = ticketvote.PluginID + "-auth-v1"
   932  	dataDescriptorVoteDetails     = ticketvote.PluginID + "-vote-v1"
   933  	dataDescriptorCastVoteDetails = ticketvote.PluginID + "-castvote-v1"
   934  	dataDescriptorVoteCollider    = ticketvote.PluginID + "-vcollider-v1"
   935  	dataDescriptorStartRunoff     = ticketvote.PluginID + "-startrunoff-v1"
   936  )
   937  
   938  // saveCommentAdd saves a CommentAdd to tstore as a plugin data blob.
   939  func (c *importCmd) saveCommentAdd(tstoreToken []byte, ca comments.CommentAdd) error {
   940  	data, err := json.Marshal(ca)
   941  	if err != nil {
   942  		return err
   943  	}
   944  	hint, err := json.Marshal(
   945  		store.DataDescriptor{
   946  			Type:       store.DataTypeStructure,
   947  			Descriptor: dataDescriptorCommentAdd,
   948  		})
   949  	if err != nil {
   950  		return err
   951  	}
   952  	be := store.NewBlobEntry(hint, data)
   953  	return c.savePluginBlobEntry(tstoreToken, be)
   954  }
   955  
   956  // saveCommentDel saves a CommentDel to tstore as a plugin data blob.
   957  func (c *importCmd) saveCommentDel(tstoreToken []byte, cd comments.CommentDel) error {
   958  	data, err := json.Marshal(cd)
   959  	if err != nil {
   960  		return err
   961  	}
   962  	hint, err := json.Marshal(
   963  		store.DataDescriptor{
   964  			Type:       store.DataTypeStructure,
   965  			Descriptor: dataDescriptorCommentDel,
   966  		})
   967  	if err != nil {
   968  		return err
   969  	}
   970  	be := store.NewBlobEntry(hint, data)
   971  	return c.savePluginBlobEntry(tstoreToken, be)
   972  }
   973  
   974  // saveCommentVote saves a CommentVote to tstore as a plugin data blob.
   975  func (c *importCmd) saveCommentVote(tstoreToken []byte, cv comments.CommentVote) error {
   976  	data, err := json.Marshal(cv)
   977  	if err != nil {
   978  		return err
   979  	}
   980  	hint, err := json.Marshal(
   981  		store.DataDescriptor{
   982  			Type:       store.DataTypeStructure,
   983  			Descriptor: dataDescriptorCommentVote,
   984  		})
   985  	if err != nil {
   986  		return err
   987  	}
   988  	be := store.NewBlobEntry(hint, data)
   989  	return c.savePluginBlobEntry(tstoreToken, be)
   990  }
   991  
   992  // saveAuthDetails saves a AuthDetails to tstore as a plugin data blob.
   993  func (c *importCmd) saveAuthDetails(tstoreToken []byte, ad ticketvote.AuthDetails) error {
   994  	data, err := json.Marshal(ad)
   995  	if err != nil {
   996  		return err
   997  	}
   998  	hint, err := json.Marshal(
   999  		store.DataDescriptor{
  1000  			Type:       store.DataTypeStructure,
  1001  			Descriptor: dataDescriptorAuthDetails,
  1002  		})
  1003  	if err != nil {
  1004  		return err
  1005  	}
  1006  	be := store.NewBlobEntry(hint, data)
  1007  	return c.savePluginBlobEntry(tstoreToken, be)
  1008  }
  1009  
  1010  // saveVoteDetails saves a VoteDetails to tstore as a plugin data blob.
  1011  func (c *importCmd) saveVoteDetails(tstoreToken []byte, vd ticketvote.VoteDetails) error {
  1012  	data, err := json.Marshal(vd)
  1013  	if err != nil {
  1014  		return err
  1015  	}
  1016  	hint, err := json.Marshal(
  1017  		store.DataDescriptor{
  1018  			Type:       store.DataTypeStructure,
  1019  			Descriptor: dataDescriptorVoteDetails,
  1020  		})
  1021  	if err != nil {
  1022  		return err
  1023  	}
  1024  	be := store.NewBlobEntry(hint, data)
  1025  	return c.savePluginBlobEntry(tstoreToken, be)
  1026  }
  1027  
  1028  // saveCastVoteDetails saves a CastVoteDetails to tstore as a plugin data blob.
  1029  func (c *importCmd) saveCastVoteDetails(tstoreToken []byte, cvd ticketvote.CastVoteDetails) error {
  1030  	data, err := json.Marshal(cvd)
  1031  	if err != nil {
  1032  		return err
  1033  	}
  1034  	hint, err := json.Marshal(
  1035  		store.DataDescriptor{
  1036  			Type:       store.DataTypeStructure,
  1037  			Descriptor: dataDescriptorCastVoteDetails,
  1038  		})
  1039  	if err != nil {
  1040  		return err
  1041  	}
  1042  	be := store.NewBlobEntry(hint, data)
  1043  	return c.savePluginBlobEntry(tstoreToken, be)
  1044  }
  1045  
  1046  // saveVoteCollider saves a voteCollider to tstore as a plugin data blob.
  1047  func (c *importCmd) saveVoteCollider(tstoreToken []byte, vc voteCollider) error {
  1048  	data, err := json.Marshal(vc)
  1049  	if err != nil {
  1050  		return err
  1051  	}
  1052  	hint, err := json.Marshal(
  1053  		store.DataDescriptor{
  1054  			Type:       store.DataTypeStructure,
  1055  			Descriptor: dataDescriptorVoteCollider,
  1056  		})
  1057  	if err != nil {
  1058  		return err
  1059  	}
  1060  	be := store.NewBlobEntry(hint, data)
  1061  	return c.savePluginBlobEntry(tstoreToken, be)
  1062  }
  1063  
  1064  // saveStartRunoffRecord saves a startRunoffRecord to tstore as a plugin data
  1065  // blob.
  1066  func (c *importCmd) saveStartRunoffRecord(tstoreToken []byte, srr startRunoffRecord) error {
  1067  	data, err := json.Marshal(srr)
  1068  	if err != nil {
  1069  		return err
  1070  	}
  1071  	hint, err := json.Marshal(
  1072  		store.DataDescriptor{
  1073  			Type:       store.DataTypeStructure,
  1074  			Descriptor: dataDescriptorStartRunoff,
  1075  		})
  1076  	if err != nil {
  1077  		return err
  1078  	}
  1079  	be := store.NewBlobEntry(hint, data)
  1080  	return c.savePluginBlobEntry(tstoreToken, be)
  1081  }
  1082  
  1083  // stubProposalUsers creates a stub in the user database for all user IDs and
  1084  // public keys found in any of the proposal data.
  1085  func (c *importCmd) stubProposalUsers(p proposal) error {
  1086  	fmt.Printf("  Stubbing proposal users...\n")
  1087  
  1088  	// Stub the proposal author
  1089  	err := c.stubUser(p.UserMetadata.UserID, p.UserMetadata.PublicKey)
  1090  	if err != nil {
  1091  		return err
  1092  	}
  1093  
  1094  	// Stub the comment and comment vote authors. A user
  1095  	// ID may be associated with multiple public keys.
  1096  	pks := make(map[string]string, 256) // [publicKey]userID
  1097  	for _, v := range p.CommentAdds {
  1098  		pks[v.PublicKey] = v.UserID
  1099  	}
  1100  	for _, v := range p.CommentDels {
  1101  		pks[v.PublicKey] = v.UserID
  1102  	}
  1103  	for _, v := range p.CommentVotes {
  1104  		pks[v.PublicKey] = v.UserID
  1105  	}
  1106  	for publicKey, userID := range pks {
  1107  		err := c.stubUser(userID, publicKey)
  1108  		if err != nil {
  1109  			return err
  1110  		}
  1111  	}
  1112  
  1113  	return nil
  1114  }
  1115  
  1116  // stubUser creates a stub in the user database for the provided user ID.
  1117  //
  1118  // If a user stub already exists, this function verifies that the stub contains
  1119  // the provided public key. If it doesn't, the function will add the missing
  1120  // public key to the user and update the stub in the database.
  1121  func (c *importCmd) stubUser(userID, publicKey string) error {
  1122  	// Check if this user already exists in the user database
  1123  	uid, err := uuid.Parse(userID)
  1124  	if err != nil {
  1125  		return err
  1126  	}
  1127  	dbu, err := c.userDB.UserGetById(uid)
  1128  	switch {
  1129  	case err == nil:
  1130  		// User already exist. Update the user if the provided
  1131  		// public key is not part of the user stub.
  1132  		for _, id := range dbu.Identities {
  1133  			if id.String() == publicKey {
  1134  				// This user stub already contains the provided
  1135  				// public key. Nothing else to do.
  1136  				return nil
  1137  			}
  1138  		}
  1139  
  1140  		fmt.Printf("    Updating stubbed user %v %v\n", uid, dbu.Username)
  1141  
  1142  		updatedIDs, err := addIdentity(dbu.Identities, publicKey)
  1143  		if err != nil {
  1144  			return err
  1145  		}
  1146  
  1147  		dbu.Identities = updatedIDs
  1148  		return c.userDB.UserUpdate(*dbu)
  1149  
  1150  	case errors.Is(err, user.ErrUserNotFound):
  1151  		// User doesn't exist. Pull their username from the mainnet
  1152  		// Politeia API and add them to the user database.
  1153  		u, err := userByID(c.http, userID)
  1154  		if err != nil {
  1155  			return err
  1156  		}
  1157  
  1158  		// Setup the identities
  1159  		ids, err := addIdentity([]user.Identity{}, publicKey)
  1160  		if err != nil {
  1161  			return err
  1162  		}
  1163  
  1164  		fmt.Printf("    Stubbing user %v %v\n", uid, u.Username)
  1165  
  1166  		return c.userDB.InsertUser(user.User{
  1167  			ID:             uid,
  1168  			Email:          u.Username + "@example.com",
  1169  			Username:       u.Username,
  1170  			HashedPassword: []byte("password"),
  1171  			Admin:          false,
  1172  			Identities:     ids,
  1173  		})
  1174  
  1175  	default:
  1176  		// All other errors
  1177  		return err
  1178  	}
  1179  }
  1180  
  1181  // parseLegacyTokens parses and returns all the unique tokens that are found in
  1182  // the file path of the provided directory or any contents of the directory.
  1183  // The tokens are returned in alphabetical order.
  1184  func parseLegacyTokens(dir string) ([]string, error) {
  1185  	tokens := make(map[string]struct{}, 1024)
  1186  	err := filepath.WalkDir(dir,
  1187  		func(path string, d fs.DirEntry, err error) error {
  1188  			token, ok := parseProposalToken(path)
  1189  			if !ok {
  1190  				return nil
  1191  			}
  1192  			tokens[token] = struct{}{}
  1193  			return nil
  1194  		})
  1195  	if err != nil {
  1196  		return nil, err
  1197  	}
  1198  
  1199  	// Convert map to a slice and sort alphabetically
  1200  	legacyTokens := make([]string, 0, len(tokens))
  1201  	for token := range tokens {
  1202  		legacyTokens = append(legacyTokens, token)
  1203  	}
  1204  	sort.SliceStable(legacyTokens, func(i, j int) bool {
  1205  		return legacyTokens[i] < legacyTokens[j]
  1206  	})
  1207  
  1208  	return legacyTokens, nil
  1209  }
  1210  
  1211  // appendMetadataStream appends the addition metadata streams onto the
  1212  // base metadata stream.
  1213  func appendMetadataStream(base, addition backend.MetadataStream) backend.MetadataStream {
  1214  	buf := bytes.NewBuffer([]byte(base.Payload))
  1215  	buf.WriteString(addition.Payload)
  1216  	base.Payload = buf.String()
  1217  	return base
  1218  }
  1219  
  1220  // decodeLegacyTokenFromFiles decodes and returns the ProposalMetadata from the
  1221  // provided files.
  1222  func decodeProposalMetadata(files []backend.File) (*pi.ProposalMetadata, error) {
  1223  	var f *backend.File
  1224  	for _, v := range files {
  1225  		if v.Name == pi.FileNameProposalMetadata {
  1226  			f = &v
  1227  			break
  1228  		}
  1229  	}
  1230  	if f == nil {
  1231  		// This should not happen
  1232  		return nil, fmt.Errorf("proposal metadata not found")
  1233  	}
  1234  	b, err := base64.StdEncoding.DecodeString(f.Payload)
  1235  	if err != nil {
  1236  		return nil, err
  1237  	}
  1238  	var pm pi.ProposalMetadata
  1239  	err = json.Unmarshal(b, &pm)
  1240  	if err != nil {
  1241  		return nil, err
  1242  	}
  1243  	return &pm, nil
  1244  }
  1245  
  1246  // convertProposalMetadataToFile converts a pi plugin ProposalMetadata into a
  1247  // backend File.
  1248  func convertProposalMetadataToFile(pm pi.ProposalMetadata) (*backend.File, error) {
  1249  	pmb, err := json.Marshal(pm)
  1250  	if err != nil {
  1251  		return nil, err
  1252  	}
  1253  	return &backend.File{
  1254  		Name:    pi.FileNameProposalMetadata,
  1255  		MIME:    mime.DetectMimeType(pmb),
  1256  		Digest:  hex.EncodeToString(util.Digest(pmb)),
  1257  		Payload: base64.StdEncoding.EncodeToString(pmb),
  1258  	}, nil
  1259  }
  1260  
  1261  // convertVoteMetadataToFile converts a ticketvote plugin VoteMetadata into a
  1262  // backend File.
  1263  func convertVoteMetadataToFile(vm ticketvote.VoteMetadata) (*backend.File, error) {
  1264  	vmb, err := json.Marshal(vm)
  1265  	if err != nil {
  1266  		return nil, err
  1267  	}
  1268  	return &backend.File{
  1269  		Name:    ticketvote.FileNameVoteMetadata,
  1270  		MIME:    mime.DetectMimeType(vmb),
  1271  		Digest:  hex.EncodeToString(util.Digest(vmb)),
  1272  		Payload: base64.StdEncoding.EncodeToString(vmb),
  1273  	}, nil
  1274  }
  1275  
  1276  // convertUserMetadataToMetadataStream converts a usermd plugin UserMetadata
  1277  // into a backend MetadataStream.
  1278  func convertUserMetadataToMetadataStream(um usermd.UserMetadata) (*backend.MetadataStream, error) {
  1279  	b, err := json.Marshal(um)
  1280  	if err != nil {
  1281  		return nil, err
  1282  	}
  1283  	return &backend.MetadataStream{
  1284  		PluginID: usermd.PluginID,
  1285  		StreamID: usermd.StreamIDUserMetadata,
  1286  		Payload:  string(b),
  1287  	}, nil
  1288  }
  1289  
  1290  // convertStatusChangeToMetadataStream converts a usermd plugin
  1291  // StatusChangeMetadata into a backend MetadataStream.
  1292  func convertStatusChangeToMetadataStream(scm usermd.StatusChangeMetadata) (*backend.MetadataStream, error) {
  1293  	b, err := json.Marshal(scm)
  1294  	if err != nil {
  1295  		return nil, err
  1296  	}
  1297  	return &backend.MetadataStream{
  1298  		PluginID: usermd.PluginID,
  1299  		StreamID: usermd.StreamIDStatusChanges,
  1300  		Payload:  string(b),
  1301  	}, nil
  1302  }
  1303  
  1304  // addIdentity converts the provided public key string into a politeiawww user
  1305  // identity and adds it to the provided identities list.
  1306  //
  1307  // The created identities will not mimic what would happen during normal
  1308  // operation of the backend and this function should only be used for creating
  1309  // test user stubs in the database.
  1310  func addIdentity(ids []user.Identity, publicKey string) ([]user.Identity, error) {
  1311  	if ids == nil {
  1312  		return nil, fmt.Errorf("identities slice is nil")
  1313  	}
  1314  
  1315  	// Add the identities to the existing identities list
  1316  	id, err := identity.PublicIdentityFromString(publicKey)
  1317  	if err != nil {
  1318  		return nil, err
  1319  	}
  1320  	ids = append(ids, user.Identity{
  1321  		Key:       id.Key,
  1322  		Activated: time.Now().Unix(),
  1323  	})
  1324  
  1325  	// Make the last identity the only active identity.
  1326  	// Not sure if this actually matters, but do it anyway.
  1327  	for i, v := range ids {
  1328  		v.Deactivated = v.Activated + 1
  1329  		ids[i] = v
  1330  	}
  1331  	ids[len(ids)-1].Deactivated = 0
  1332  
  1333  	return ids, nil
  1334  }