github.com/decred/politeia@v1.4.0/politeiad/backend/gitbe/cms.go (about)

     1  // Copyright (c) 2020 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 gitbe
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/hex"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"path/filepath"
    16  	"strconv"
    17  	"strings"
    18  	"time"
    19  
    20  	"github.com/decred/politeia/politeiad/api/v1/identity"
    21  	"github.com/decred/politeia/politeiad/backend"
    22  	"github.com/decred/politeia/politeiad/backend/gitbe/cmsplugin"
    23  	"github.com/decred/politeia/util"
    24  )
    25  
    26  const (
    27  	cmsPluginIdentity    = "cmsfullidentity"
    28  	cmsPluginJournals    = "cmssjournals"
    29  	cmsPluginEnableCache = "enablecache"
    30  
    31  	defaultCMSBallotFilename = "cms.ballot.journal"
    32  	defaultCMSBallotFlushed  = "cms.ballot.flushed"
    33  )
    34  
    35  type CastDCCVoteJournal struct {
    36  	CastVote cmsplugin.CastVote `json:"castvote"` // Client side vote
    37  	Receipt  string             `json:"receipt"`  // Signature of CastVote.Signature
    38  }
    39  
    40  func encodeCastDCCVoteJournal(cvj CastDCCVoteJournal) ([]byte, error) {
    41  	b, err := json.Marshal(cvj)
    42  	if err != nil {
    43  		return nil, err
    44  	}
    45  
    46  	return b, nil
    47  }
    48  
    49  var (
    50  	cmsPluginSettings map[string]string             // [key]setting
    51  	cmsPluginHooks    map[string]func(string) error // [key]func(token) error
    52  
    53  	// Cached values, requires lock. These caches are lazy loaded.
    54  	cmsPluginVoteCache         = make(map[string]cmsplugin.StartVote)      // [token]startvote
    55  	cmsPluginVoteSnapshotCache = make(map[string]cmsplugin.StartVoteReply) // [token]StartVoteReply
    56  
    57  	// Cached values, requires lock. These caches are built on startup.
    58  	cmsPluginVotesCache = make(map[string]map[string]struct{}) // [token][ticket]struct{}
    59  
    60  	// errIneligibleUserID is emitted when a vote is cast using an
    61  	// ineligible userid.
    62  	errIneligibleUserID = errors.New("ineligible userid")
    63  )
    64  
    65  func getCMSPlugin(testnet bool) backend.Plugin {
    66  	cmsPlugin := backend.Plugin{
    67  		ID:       cmsplugin.ID,
    68  		Version:  cmsplugin.Version,
    69  		Settings: []backend.PluginSetting{},
    70  	}
    71  
    72  	cmsPlugin.Settings = append(cmsPlugin.Settings,
    73  		backend.PluginSetting{
    74  			Key:   cmsPluginEnableCache,
    75  			Value: "",
    76  		})
    77  
    78  	// Initialize hooks
    79  	cmsPluginHooks = make(map[string]func(string) error)
    80  
    81  	// Initialize settings map
    82  	cmsPluginSettings = make(map[string]string)
    83  	for _, v := range cmsPlugin.Settings {
    84  		cmsPluginSettings[v.Key] = v.Value
    85  	}
    86  	return cmsPlugin
    87  }
    88  
    89  // initDecredPluginJournals is called externally to run initial procedures
    90  // such as replaying journals
    91  func (g *gitBackEnd) initCMSPluginJournals() error {
    92  	log.Infof("initCMSPluginJournals")
    93  
    94  	// check if backend journal is initialized
    95  	if g.journal == nil {
    96  		return fmt.Errorf("initCMSPlugin backend journal isn't initialized")
    97  	}
    98  
    99  	err := g.replayAllJournals()
   100  	if err != nil {
   101  		log.Infof("initCMSPlugin replay all journals %v", err)
   102  	}
   103  	return nil
   104  }
   105  
   106  // SetCMSPluginSetting removes a setting if the value is "" and adds a setting otherwise.
   107  func setCMSPluginSetting(key, value string) {
   108  	if value == "" {
   109  		delete(cmsPluginSettings, key)
   110  		return
   111  	}
   112  
   113  	cmsPluginSettings[key] = value
   114  }
   115  
   116  // flushDCCVotes flushes votes journal to cms plugin directory in git. It
   117  // returns the filename that was coppied into git repo.
   118  //
   119  // Must be called WITH the mutex held.
   120  func (g *gitBackEnd) flushDCCVotes(token string) (string, error) {
   121  	if !g.vettedPropExists(token) {
   122  		return "", fmt.Errorf("unknown dcc: %v", token)
   123  	}
   124  
   125  	// Setup source filenames and verify they actually exist
   126  	srcDir := pijoin(g.journals, token)
   127  	srcVotes := pijoin(srcDir, defaultCMSBallotFilename)
   128  	if !util.FileExists(srcVotes) {
   129  		return "", nil
   130  	}
   131  
   132  	// Setup destination filenames
   133  	version, err := getLatest(pijoin(g.unvetted, token))
   134  	if err != nil {
   135  		return "", err
   136  	}
   137  	dir := pijoin(g.unvetted, token, version, pluginDataDir)
   138  	votes := pijoin(dir, defaultCMSBallotFilename)
   139  
   140  	// Create the destination container dir
   141  	_ = os.MkdirAll(dir, 0764)
   142  
   143  	// Move journal into place
   144  	err = g.journal.Copy(srcVotes, votes)
   145  	if err != nil {
   146  		return "", err
   147  	}
   148  
   149  	// Return filename that is relative to git dir.
   150  	return pijoin(token, version, pluginDataDir, defaultCMSBallotFilename), nil
   151  }
   152  
   153  // _flushDCCVotesJournals walks all votes journal directories and copies
   154  // modified journals into the unvetted repo. It returns an array of filenames
   155  // that need to be added to the git repo and subsequently rebased into the
   156  // vetted repo .
   157  //
   158  // Must be called WITH the mutex held.
   159  func (g *gitBackEnd) _flushDCCVotesJournals() ([]string, error) {
   160  	dirs, err := os.ReadDir(g.journals)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  
   165  	files := make([]string, 0, len(dirs))
   166  	for _, v := range dirs {
   167  		filename := pijoin(g.journals, v.Name(),
   168  			defaultCMSBallotFlushed)
   169  		log.Tracef("Checking: %v", v.Name())
   170  		if util.FileExists(filename) {
   171  			continue
   172  		}
   173  
   174  		log.Infof("Flushing votes: %v", v.Name())
   175  
   176  		// We simply copy the journal into git
   177  		destination, err := g.flushDCCVotes(v.Name())
   178  		if err != nil {
   179  			log.Errorf("Could not flush %v: %v", v.Name(), err)
   180  			continue
   181  		}
   182  
   183  		// Create flush record
   184  		err = createFlushFile(filename)
   185  		if err != nil {
   186  			log.Errorf("Could not mark flushed %v: %v", v.Name(),
   187  				err)
   188  			continue
   189  		}
   190  
   191  		// Add filename to work
   192  		files = append(files, destination)
   193  	}
   194  
   195  	return files, nil
   196  }
   197  
   198  // flushDCCVoteJournals wraps _flushDCCVoteJournals in git magic to revert
   199  // flush in case of errors.
   200  //
   201  // Must be called WITHOUT the mutex held.
   202  func (g *gitBackEnd) flushDCCVoteJournals() error {
   203  	log.Tracef("flushDCCVoteJournals")
   204  
   205  	// We may have to make this more granular
   206  	g.Lock()
   207  	defer g.Unlock()
   208  
   209  	// git checkout master
   210  	err := g.gitCheckout(g.unvetted, "master")
   211  	if err != nil {
   212  		return err
   213  	}
   214  
   215  	// git pull --ff-only --rebase
   216  	err = g.gitPull(g.unvetted, true)
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	// git checkout -b timestamp_flushvotes
   222  	branch := strconv.FormatInt(time.Now().Unix(), 10) + "_flushvotes"
   223  	_ = g.gitBranchDelete(g.unvetted, branch) // Just in case
   224  	err = g.gitNewBranch(g.unvetted, branch)
   225  	if err != nil {
   226  		return err
   227  	}
   228  
   229  	// closure to handle unwind if needed
   230  	var errUnwind error
   231  	defer func() {
   232  		if errUnwind == nil {
   233  			return
   234  		}
   235  		err := g.flushJournalsUnwind(branch)
   236  		if err != nil {
   237  			log.Errorf("flushJournalsUnwind: %v", err)
   238  		}
   239  	}()
   240  
   241  	// Flush journals
   242  	files, err := g._flushDCCVotesJournals()
   243  	if err != nil {
   244  		errUnwind = err
   245  		return err
   246  	}
   247  
   248  	if len(files) == 0 {
   249  		log.Info("flushVotesJournals: nothing to do")
   250  		err = g.flushJournalsUnwind(branch)
   251  		if err != nil {
   252  			log.Errorf("flushJournalsUnwind: %v", err)
   253  		}
   254  		return nil
   255  	}
   256  
   257  	// git add journals
   258  	commitMessage := "Flush vote journals.\n\n"
   259  	for _, v := range files {
   260  		err = g.gitAdd(g.unvetted, v)
   261  		if err != nil {
   262  			errUnwind = err
   263  			return err
   264  		}
   265  
   266  		s := strings.Split(v, string(os.PathSeparator))
   267  		if len(s) == 0 {
   268  			commitMessage += "ERROR: " + v + "\n"
   269  		} else {
   270  			commitMessage += s[0] + "\n"
   271  		}
   272  	}
   273  
   274  	// git commit
   275  	err = g.gitCommit(g.unvetted, commitMessage)
   276  	if err != nil {
   277  		errUnwind = err
   278  		return err
   279  	}
   280  
   281  	// git rebase master
   282  	err = g.rebasePR(branch)
   283  	if err != nil {
   284  		errUnwind = err
   285  		return err
   286  	}
   287  
   288  	return nil
   289  }
   290  func (g *gitBackEnd) cmsPluginJournalFlusher() {
   291  	// XXX make this a single PR instead of 2 to save some git time
   292  	err := g.flushDCCVoteJournals()
   293  	if err != nil {
   294  		log.Errorf("cmsPluginVoteFlusher: %v", err)
   295  	}
   296  }
   297  
   298  func (g *gitBackEnd) pluginStartDCCVote(payload string) (string, error) {
   299  	vote, err := cmsplugin.DecodeStartVote([]byte(payload))
   300  	if err != nil {
   301  		return "", fmt.Errorf("DecodeStartVote %v", err)
   302  	}
   303  
   304  	// Verify vote bits are somewhat sane
   305  	for _, v := range vote.Vote.Options {
   306  		err = _validateCMSVoteBit(vote.Vote.Options, vote.Vote.Mask, v.Bits)
   307  		if err != nil {
   308  			return "", fmt.Errorf("invalid vote bits: %v", err)
   309  		}
   310  	}
   311  
   312  	// Verify dcc exists
   313  	tokenB, err := util.ConvertStringToken(vote.Vote.Token)
   314  	if err != nil {
   315  		return "", fmt.Errorf("ConvertStringToken %v", err)
   316  	}
   317  	token := vote.Vote.Token
   318  
   319  	if !g.vettedPropExists(token) {
   320  		return "", fmt.Errorf("unknown proposal: %v", token)
   321  	}
   322  
   323  	// Make sure vote duration is within min/max range
   324  	// XXX calculate this value for testnet instead of using hard coded values.
   325  	if vote.Vote.Duration < cmsplugin.VoteDurationMin ||
   326  		vote.Vote.Duration > cmsplugin.VoteDurationMax {
   327  		// XXX return a user error instead of an internal error
   328  		return "", fmt.Errorf("invalid duration: %v (%v - %v)",
   329  			vote.Vote.Duration, cmsplugin.VoteDurationMin,
   330  			cmsplugin.VoteDurationMax)
   331  	}
   332  
   333  	// 1. Get best block
   334  	bb, err := bestBlock()
   335  	if err != nil {
   336  		return "", fmt.Errorf("bestBlock %v", err)
   337  	}
   338  	if bb.Height < uint32(g.activeNetParams.TicketMaturity) {
   339  		return "", fmt.Errorf("invalid height")
   340  	}
   341  	// 2. Subtract TicketMaturity from block height to get into
   342  	// unforkable teritory
   343  	startVoteBlock, err := block(bb.Height)
   344  	if err != nil {
   345  		return "", fmt.Errorf("bestBlock %v", err)
   346  	}
   347  
   348  	svr := cmsplugin.StartVoteReply{
   349  		Version:          cmsplugin.VersionStartVoteReply,
   350  		StartBlockHeight: startVoteBlock.Height,
   351  		StartBlockHash:   startVoteBlock.Hash,
   352  		EndHeight:        startVoteBlock.Height + vote.Vote.Duration,
   353  	}
   354  	svrb, err := cmsplugin.EncodeStartVoteReply(svr)
   355  	if err != nil {
   356  		return "", fmt.Errorf("EncodeStartVoteReply: %v", err)
   357  	}
   358  
   359  	// Add version to on disk structure
   360  	vote.Version = cmsplugin.VersionStartVote
   361  	voteb, err := cmsplugin.EncodeStartVote(vote)
   362  	if err != nil {
   363  		return "", fmt.Errorf("EncodeStartVote: %v", err)
   364  	}
   365  
   366  	// Verify proposal state
   367  	g.Lock()
   368  	defer g.Unlock()
   369  	if g.shutdown {
   370  		// Make sure we are not shutting down
   371  		return "", backend.ErrShutdown
   372  	}
   373  
   374  	// Verify DCC vote state
   375  	vbExists := g.vettedMetadataStreamExists(tokenB,
   376  		cmsplugin.MDStreamVoteBits)
   377  	vsExists := g.vettedMetadataStreamExists(tokenB,
   378  		cmsplugin.MDStreamVoteSnapshot)
   379  
   380  	switch {
   381  	case vbExists && vsExists:
   382  		// Vote has started
   383  		return "", fmt.Errorf("dcc vote already started: %v", token)
   384  	case !vbExists && !vsExists:
   385  		// Vote has not started; continue
   386  	default:
   387  		// We're in trouble!
   388  		return "", fmt.Errorf("dcc vote is unknown vote state: %v",
   389  			token)
   390  	}
   391  
   392  	// Store snapshot in metadata
   393  	err = g._updateVettedMetadata(tokenB, nil, []backend.MetadataStream{
   394  		{
   395  			ID:      cmsplugin.MDStreamVoteBits,
   396  			Payload: string(voteb),
   397  		},
   398  		{
   399  			ID:      cmsplugin.MDStreamVoteSnapshot,
   400  			Payload: string(svrb),
   401  		}})
   402  	if err != nil {
   403  		return "", fmt.Errorf("_updateVettedMetadata: %v", err)
   404  	}
   405  
   406  	// Add vote snapshot to in-memory cache
   407  	cmsPluginVoteSnapshotCache[token] = svr
   408  
   409  	log.Infof("Vote started for: %v snapshot %v start %v end %v",
   410  		token, svr.StartBlockHash, svr.StartBlockHeight,
   411  		svr.EndHeight)
   412  
   413  	// return success and encoded answer
   414  	return string(svrb), nil
   415  }
   416  
   417  // validateCMSVoteBits ensures that the passed in bit is a valid vote option.
   418  // This function is expensive due to it's filesystem touches and therefore is
   419  // lazily cached. This could stand a rewrite.
   420  func (g *gitBackEnd) validateCMSVoteBit(token, bit string) error {
   421  	b, err := strconv.ParseUint(bit, 16, 64)
   422  	if err != nil {
   423  		return err
   424  	}
   425  
   426  	g.Lock()
   427  	defer g.Unlock()
   428  	if g.shutdown {
   429  		return backend.ErrShutdown
   430  	}
   431  
   432  	sv, ok := cmsPluginVoteCache[token]
   433  	if !ok {
   434  		// StartVote is not in the cache. Load it from disk.
   435  
   436  		// git checkout master
   437  		err = g.gitCheckout(g.unvetted, "master")
   438  		if err != nil {
   439  			return err
   440  		}
   441  
   442  		// git pull --ff-only --rebase
   443  		err = g.gitPull(g.unvetted, true)
   444  		if err != nil {
   445  			return err
   446  		}
   447  		// Load md stream
   448  		svb, err := os.ReadFile(mdFilename(g.vetted, token,
   449  			cmsplugin.MDStreamVoteBits))
   450  		if err != nil {
   451  			return err
   452  		}
   453  		svp, err := cmsplugin.DecodeStartVote(svb)
   454  		if err != nil {
   455  			return err
   456  		}
   457  		sv = svp
   458  
   459  		// Update cache
   460  		cmsPluginVoteCache[token] = sv
   461  	}
   462  
   463  	// Handle StartVote versioning
   464  	var (
   465  		mask    uint64
   466  		options []cmsplugin.VoteOption
   467  	)
   468  	switch sv.Version {
   469  	case cmsplugin.VersionStartVote:
   470  		mask = sv.Vote.Mask
   471  		options = sv.Vote.Options
   472  	default:
   473  		return fmt.Errorf("invalid start vote version %v %v",
   474  			sv.Version, sv.Token)
   475  	}
   476  
   477  	return _validateCMSVoteBit(options, mask, b)
   478  }
   479  
   480  type invalidVoteBitError struct {
   481  	err error
   482  }
   483  
   484  func (i invalidVoteBitError) Error() string {
   485  	return i.err.Error()
   486  }
   487  
   488  // _validateVoteBit iterates over all vote bits and ensure the sent in vote bit
   489  // exists.
   490  func _validateCMSVoteBit(options []cmsplugin.VoteOption, mask uint64, bit uint64) error {
   491  	if len(options) == 0 {
   492  		return fmt.Errorf("_validateVoteBit vote corrupt")
   493  	}
   494  	if bit == 0 {
   495  		return invalidVoteBitError{
   496  			err: fmt.Errorf("invalid bit 0x%x", bit),
   497  		}
   498  	}
   499  	if mask&bit != bit {
   500  		return invalidVoteBitError{
   501  			err: fmt.Errorf("invalid mask 0x%x bit 0x%x",
   502  				mask, bit),
   503  		}
   504  	}
   505  	for _, v := range options {
   506  		if v.Bits == bit {
   507  			return nil
   508  		}
   509  		if v.Id != cmsplugin.DCCApprovalString &&
   510  			v.Id != cmsplugin.DCCDisapprovalString {
   511  			return invalidVoteBitError{
   512  				err: fmt.Errorf("bit option not valid found: %s", v.Id),
   513  			}
   514  		}
   515  	}
   516  	return invalidVoteBitError{
   517  		err: fmt.Errorf("bit not found 0x%x", bit),
   518  	}
   519  }
   520  
   521  // replayDCCBallot replays voting journal for given dcc.
   522  //
   523  // Functions must be called WITH the lock held.
   524  func (g *gitBackEnd) replayDCCBallot(token string) error {
   525  	// Verify proposal exists, we can run this lockless
   526  	if !g.vettedPropExists(token) {
   527  		return nil
   528  	}
   529  
   530  	// Do some cheap things before expensive calls
   531  	bfilename := pijoin(g.journals, token,
   532  		defaultCMSBallotFilename)
   533  
   534  	// Replay journal
   535  	err := g.journal.Open(bfilename)
   536  	if err != nil {
   537  		if !os.IsNotExist(err) {
   538  			return fmt.Errorf("journal.Open: %v", err)
   539  		}
   540  		return nil
   541  	}
   542  	defer func() {
   543  		err = g.journal.Close(bfilename)
   544  		if err != nil {
   545  			log.Errorf("journal.Close: %v", err)
   546  		}
   547  	}()
   548  
   549  	for {
   550  		err = g.journal.Replay(bfilename, func(s string) error {
   551  			ss := bytes.NewReader([]byte(s))
   552  			d := json.NewDecoder(ss)
   553  
   554  			// Decode action
   555  			var action JournalAction
   556  			err = d.Decode(&action)
   557  			if err != nil {
   558  				return fmt.Errorf("journal action: %v", err)
   559  			}
   560  
   561  			switch action.Action {
   562  			case journalActionAdd:
   563  				var cvj CastDCCVoteJournal
   564  				err = d.Decode(&cvj)
   565  				if err != nil {
   566  					return fmt.Errorf("journal add: %v",
   567  						err)
   568  				}
   569  
   570  				token := cvj.CastVote.Token
   571  				userid := cvj.CastVote.UserID
   572  				// See if the prop already exists
   573  				if _, ok := cmsPluginVotesCache[token]; !ok {
   574  					// Create map to track tickets
   575  					cmsPluginVotesCache[token] = make(map[string]struct{})
   576  				}
   577  				// See if we have a duplicate vote
   578  				if _, ok := cmsPluginVotesCache[token][userid]; ok {
   579  					log.Errorf("duplicate cms cast vote %v %v",
   580  						token, userid)
   581  				}
   582  				// All good, record vote in cache
   583  				cmsPluginVotesCache[token][userid] = struct{}{}
   584  
   585  			default:
   586  				return fmt.Errorf("invalid action: %v",
   587  					action.Action)
   588  			}
   589  			return nil
   590  		})
   591  		if errors.Is(err, io.EOF) {
   592  			break
   593  		} else if err != nil {
   594  			return err
   595  		}
   596  	}
   597  
   598  	return nil
   599  }
   600  
   601  // loadDCCVoteCache loads the cmsplugin.StartVote from disk for the provided
   602  // token and adds it to the cmsPluginVoteCache.
   603  //
   604  // This function must be called WITH the lock held.
   605  func (g *gitBackEnd) loadDCCVoteCache(token string) (*cmsplugin.StartVote, error) {
   606  	// git checkout master
   607  	err := g.gitCheckout(g.unvetted, "master")
   608  	if err != nil {
   609  		return nil, err
   610  	}
   611  
   612  	// git pull --ff-only --rebase
   613  	err = g.gitPull(g.unvetted, true)
   614  	if err != nil {
   615  		return nil, err
   616  	}
   617  
   618  	// Load the vote snapshot from disk
   619  	f, err := os.Open(mdFilename(g.vetted, token,
   620  		cmsplugin.MDStreamVoteBits))
   621  	if err != nil {
   622  		return nil, err
   623  	}
   624  	defer f.Close()
   625  
   626  	var sv cmsplugin.StartVote
   627  	d := json.NewDecoder(f)
   628  	err = d.Decode(&sv)
   629  	if err != nil {
   630  		return nil, err
   631  	}
   632  
   633  	cmsPluginVoteCache[token] = sv
   634  
   635  	return &sv, nil
   636  }
   637  
   638  // loadDCCVoteSnapshotCache loads the cmsplugin.StartVoteReply from disk for the provided
   639  // token and adds it to the cmsPluginVoteSnapshotCache.
   640  //
   641  // This function must be called WITH the lock held.
   642  func (g *gitBackEnd) loadDCCVoteSnapshotCache(token string) (*cmsplugin.StartVoteReply, error) {
   643  	// git checkout master
   644  	err := g.gitCheckout(g.unvetted, "master")
   645  	if err != nil {
   646  		return nil, err
   647  	}
   648  
   649  	// git pull --ff-only --rebase
   650  	err = g.gitPull(g.unvetted, true)
   651  	if err != nil {
   652  		return nil, err
   653  	}
   654  
   655  	// Load the vote snapshot from disk
   656  	f, err := os.Open(mdFilename(g.vetted, token,
   657  		cmsplugin.MDStreamVoteSnapshot))
   658  	if err != nil {
   659  		return nil, err
   660  	}
   661  	defer f.Close()
   662  
   663  	var svr cmsplugin.StartVoteReply
   664  	d := json.NewDecoder(f)
   665  	err = d.Decode(&svr)
   666  	if err != nil {
   667  		return nil, err
   668  	}
   669  
   670  	cmsPluginVoteSnapshotCache[token] = svr
   671  
   672  	return &svr, nil
   673  }
   674  
   675  // dccVoteEndHeight returns the voting period end height for the provided token.
   676  func (g *gitBackEnd) dccVoteEndHeight(token string) (uint32, error) {
   677  	g.Lock()
   678  	defer g.Unlock()
   679  	if g.shutdown {
   680  		return 0, backend.ErrShutdown
   681  	}
   682  
   683  	svr, ok := cmsPluginVoteSnapshotCache[token]
   684  	if !ok {
   685  		s, err := g.loadDCCVoteSnapshotCache(token)
   686  		if err != nil {
   687  			return 0, err
   688  		}
   689  		svr = *s
   690  	}
   691  
   692  	return svr.EndHeight, nil
   693  }
   694  
   695  // writeDCCVote writes the provided vote to the provided journal file path, if the
   696  // vote does not already exist. Once successfully written to the journal, the
   697  // vote is added to the cast vote memory cache.
   698  //
   699  // This function must be called WITHOUT the lock held.
   700  func (g *gitBackEnd) writeDCCVote(v cmsplugin.CastVote, receipt, journalPath string) error {
   701  	g.Lock()
   702  	defer g.Unlock()
   703  
   704  	// Ensure ticket is eligible to vote.
   705  	// This cache should have already been loaded when the
   706  	// vote end height was validated, but lets be sure.
   707  	sv, ok := cmsPluginVoteCache[v.Token]
   708  	if !ok {
   709  		s, err := g.loadDCCVoteCache(v.Token)
   710  		if err != nil {
   711  			return fmt.Errorf("loadDCCVoteCache: %v",
   712  				err)
   713  		}
   714  		sv = *s
   715  	}
   716  	var found bool
   717  	for _, t := range sv.UserWeights {
   718  		if t.UserID == v.UserID {
   719  			found = true
   720  			break
   721  		}
   722  	}
   723  	if !found {
   724  		return errIneligibleUserID
   725  	}
   726  
   727  	// Ensure vote is not a duplicate
   728  	_, ok = cmsPluginVotesCache[v.Token]
   729  	if !ok {
   730  		cmsPluginVotesCache[v.Token] = make(map[string]struct{})
   731  	}
   732  
   733  	_, ok = cmsPluginVotesCache[v.Token][v.UserID]
   734  	if ok {
   735  		return errDuplicateVote
   736  	}
   737  
   738  	// Create journal entry
   739  	cvj := CastDCCVoteJournal{
   740  		CastVote: v,
   741  		Receipt:  receipt,
   742  	}
   743  	blob, err := encodeCastDCCVoteJournal(cvj)
   744  	if err != nil {
   745  		return fmt.Errorf("encodeCastVoteJournal: %v",
   746  			err)
   747  	}
   748  
   749  	// Write vote to journal
   750  	err = g.journal.Journal(journalPath, string(journalAdd)+
   751  		string(blob))
   752  	if err != nil {
   753  		return fmt.Errorf("could not journal vote %v: %v %v",
   754  			v.Token, v.UserID, err)
   755  	}
   756  
   757  	// Add vote to memory cache
   758  	cmsPluginVotesCache[v.Token][v.UserID] = struct{}{}
   759  
   760  	return nil
   761  }
   762  
   763  func (g *gitBackEnd) pluginCastVote(payload string) (string, error) {
   764  	log.Tracef("pluginCastVote")
   765  
   766  	// Check if journals were replayed
   767  	if !journalsReplayed {
   768  		return "", backend.ErrJournalsNotReplayed
   769  	}
   770  
   771  	// Decode ballot
   772  	vote, err := cmsplugin.DecodeCastVote([]byte(payload))
   773  	if err != nil {
   774  		return "", fmt.Errorf("DecodeBallot: %v", err)
   775  	}
   776  
   777  	// XXX this should become part of some sort of context
   778  	fiJSON, ok := cmsPluginSettings[cmsPluginIdentity]
   779  	if !ok {
   780  		return "", fmt.Errorf("full identity not set")
   781  	}
   782  	fi, err := identity.UnmarshalFullIdentity([]byte(fiJSON))
   783  	if err != nil {
   784  		return "", err
   785  	}
   786  
   787  	// Get best block
   788  	bb, err := bestBlock()
   789  	if err != nil {
   790  		return "", fmt.Errorf("bestBlock %v", err)
   791  	}
   792  
   793  	br := cmsplugin.CastVoteReply{}
   794  	// Verify proposal exists, we can run this lockless
   795  	if !g.vettedPropExists(vote.Token) {
   796  		log.Errorf("pluginCastVote: proposal not found: %v",
   797  			vote.Token)
   798  		e := cmsplugin.ErrorStatusDCCNotFound
   799  		err := fmt.Sprintf("%v: %v",
   800  			cmsplugin.ErrorStatus[e], vote.Token)
   801  		return "", fmt.Errorf("write vote: %v", err)
   802  	}
   803  
   804  	// Ensure that the votebits are correct
   805  	err = g.validateCMSVoteBit(vote.Token, vote.VoteBit)
   806  	if err != nil {
   807  		var ierr invalidVoteBitError
   808  		if errors.As(err, &ierr) {
   809  			es := cmsplugin.ErrorStatusInvalidVoteBit
   810  			err := fmt.Sprintf("%v: %v",
   811  				cmsplugin.ErrorStatus[es], ierr.err.Error())
   812  			return "", fmt.Errorf("validateCMSVoteBit: %v", err)
   813  
   814  		}
   815  		t := time.Now().Unix()
   816  		log.Errorf("pluginCastVote: validateCMSVoteBit %v %v %v %v",
   817  			vote.UserID, vote.Token, t, err)
   818  		e := cmsplugin.ErrorStatusInternalError
   819  		err := fmt.Sprintf("%v: %v",
   820  			cmsplugin.ErrorStatus[e], t)
   821  		return "", fmt.Errorf("write vote: %v", err)
   822  
   823  	}
   824  
   825  	// Verify voting period has not ended
   826  	endHeight, err := g.dccVoteEndHeight(vote.Token)
   827  	if err != nil {
   828  		t := time.Now().Unix()
   829  		log.Errorf("pluginCastVote: dccVoteEndHeight %v %v %v %v",
   830  			vote.UserID, vote.Token, t, err)
   831  		e := cmsplugin.ErrorStatusInternalError
   832  		err := fmt.Sprintf("%v: %v",
   833  			cmsplugin.ErrorStatus[e], t)
   834  		return "", fmt.Errorf("write vote: %v", err)
   835  
   836  	}
   837  	if bb.Height >= endHeight {
   838  		e := cmsplugin.ErrorStatusVoteHasEnded
   839  		br.ErrorStatus = e
   840  		err := fmt.Sprintf("%v: %v",
   841  			cmsplugin.ErrorStatus[e], vote.Token)
   842  		return "", fmt.Errorf("write vote: %v", err)
   843  
   844  	}
   845  
   846  	// Ensure journal directory exists
   847  	dir := pijoin(g.journals, vote.Token)
   848  	bfilename := pijoin(dir, defaultCMSBallotFilename)
   849  	err = os.MkdirAll(dir, 0774)
   850  	if err != nil {
   851  		// Should not fail, so return failure to alert people
   852  		return "", fmt.Errorf("make journal dir: %v", err)
   853  	}
   854  
   855  	// Sign signature
   856  	r := fi.SignMessage([]byte(vote.Signature))
   857  	receipt := hex.EncodeToString(r[:])
   858  
   859  	// Write vote to journal
   860  	err = g.writeDCCVote(*vote, receipt, bfilename)
   861  	if err != nil {
   862  		switch err {
   863  		case errDuplicateVote:
   864  			e := cmsplugin.ErrorStatusDuplicateVote
   865  			err := fmt.Sprintf("%v: %v",
   866  				cmsplugin.ErrorStatus[e], vote.Token)
   867  			return "", fmt.Errorf("write vote: %v", err)
   868  		case errIneligibleUserID:
   869  			e := cmsplugin.ErrorStatusIneligibleUserID
   870  			err := fmt.Sprintf("%v: %v",
   871  				cmsplugin.ErrorStatus[e], vote.Token)
   872  			return "", fmt.Errorf("write vote: %v", err)
   873  		default:
   874  			// Should not fail, so return failure to alert people
   875  			return "", fmt.Errorf("write vote: %v", err)
   876  		}
   877  	}
   878  
   879  	// Update reply
   880  	br.ClientSignature = vote.Signature
   881  	br.Signature = receipt
   882  
   883  	// Mark comment journal dirty
   884  	flushFilename := pijoin(g.journals, vote.Token,
   885  		defaultCMSBallotFlushed)
   886  	_ = os.Remove(flushFilename)
   887  
   888  	// Encode reply
   889  	brb, err := cmsplugin.EncodeCastVoteReply(br)
   890  	if err != nil {
   891  		return "", fmt.Errorf("EncodeCastVoteReply: %v", err)
   892  	}
   893  
   894  	// return success and encoded answer
   895  	return string(brb), nil
   896  }
   897  
   898  // tallyDCCVotes replays the ballot journal for a proposal and tallies the votes.
   899  //
   900  // Function must be called WITH the lock held.
   901  func (g *gitBackEnd) tallyDCCVotes(token string) ([]cmsplugin.CastVote, error) {
   902  	// Do some cheap things before expensive calls
   903  	bfilename := pijoin(g.journals, token, defaultCMSBallotFilename)
   904  
   905  	// Replay journal
   906  	err := g.journal.Open(bfilename)
   907  	if err != nil {
   908  		if !os.IsNotExist(err) {
   909  			return nil, fmt.Errorf("journal.Open: %v", err)
   910  		}
   911  		return []cmsplugin.CastVote{}, nil
   912  	}
   913  	defer func() {
   914  		err = g.journal.Close(bfilename)
   915  		if err != nil {
   916  			log.Errorf("journal.Close: %v", err)
   917  		}
   918  	}()
   919  
   920  	cv := make([]cmsplugin.CastVote, 0, 41000)
   921  	for {
   922  		err = g.journal.Replay(bfilename, func(s string) error {
   923  			ss := bytes.NewReader([]byte(s))
   924  			d := json.NewDecoder(ss)
   925  
   926  			// Decode action
   927  			var action JournalAction
   928  			err = d.Decode(&action)
   929  			if err != nil {
   930  				return fmt.Errorf("journal action: %v", err)
   931  			}
   932  
   933  			switch action.Action {
   934  			case journalActionAdd:
   935  				var cvj CastDCCVoteJournal
   936  				err = d.Decode(&cvj)
   937  				if err != nil {
   938  					return fmt.Errorf("journal add: %v",
   939  						err)
   940  				}
   941  				cv = append(cv, cvj.CastVote)
   942  
   943  			default:
   944  				return fmt.Errorf("invalid action: %v",
   945  					action.Action)
   946  			}
   947  			return nil
   948  		})
   949  		if errors.Is(err, io.EOF) {
   950  			break
   951  		} else if err != nil {
   952  			return nil, err
   953  		}
   954  	}
   955  
   956  	return cv, nil
   957  }
   958  
   959  // pluginDCCVoteDetails returns the VoteDetails of a requested DCC vote.
   960  // It uses the caches that should be populated with the StartVotes and
   961  // StartVoteReplies.
   962  func (g *gitBackEnd) pluginDCCVoteDetails(payload string) (string, error) {
   963  	log.Tracef("pluginDCCVoteDetails: %v", payload)
   964  
   965  	vd, err := cmsplugin.DecodeVoteDetails([]byte(payload))
   966  	if err != nil {
   967  		return "", fmt.Errorf("DecodeVoteResults %v", err)
   968  	}
   969  
   970  	// Verify dcc exists, we can run this lockless
   971  	if !g.vettedPropExists(vd.Token) {
   972  		return "", fmt.Errorf("dcc not found: %v", vd.Token)
   973  	}
   974  
   975  	token, err := hex.DecodeString(vd.Token)
   976  	if err != nil {
   977  		return "", err
   978  	}
   979  	// Find the most recent vesion number for this record
   980  	r, err := g.GetVetted(token, "")
   981  	if err != nil {
   982  		return "", fmt.Errorf("GetVetted %v version 0: %v", token, err)
   983  	}
   984  
   985  	var vdr cmsplugin.VoteDetailsReply
   986  	// Prepare reply
   987  	for _, v := range r.Metadata {
   988  		switch v.ID {
   989  		case cmsplugin.MDStreamVoteBits:
   990  			// Start vote
   991  			sv, err := cmsplugin.DecodeStartVote([]byte(v.Payload))
   992  			if err != nil {
   993  				return "", err
   994  			}
   995  			vdr.StartVote = sv
   996  		case cmsplugin.MDStreamVoteSnapshot:
   997  			svr, err := cmsplugin.DecodeStartVoteReply([]byte(v.Payload))
   998  			if err != nil {
   999  				return "", err
  1000  			}
  1001  			vdr.StartVoteReply = svr
  1002  		}
  1003  	}
  1004  
  1005  	reply, err := cmsplugin.EncodeVoteDetailsReply(vdr)
  1006  	if err != nil {
  1007  		return "", fmt.Errorf("Could not encode VoteResultsReply: %v",
  1008  			err)
  1009  	}
  1010  	return string(reply), nil
  1011  }
  1012  
  1013  // pluginDCCVoteSummary
  1014  func (g *gitBackEnd) pluginDCCVoteSummary(payload string) (string, error) {
  1015  	log.Tracef("pluginDCCVoteSummary: %v", payload)
  1016  
  1017  	vs, err := cmsplugin.DecodeVoteSummary([]byte(payload))
  1018  	if err != nil {
  1019  		return "", fmt.Errorf("DecodeVoteResults %v", err)
  1020  	}
  1021  
  1022  	// Verify dcc exists, we can run this lockless
  1023  	if !g.vettedPropExists(vs.Token) {
  1024  		return "", fmt.Errorf("dcc not found: %v", vs.Token)
  1025  	}
  1026  
  1027  	token, err := hex.DecodeString(vs.Token)
  1028  	if err != nil {
  1029  		return "", err
  1030  	}
  1031  	// Find the most recent vesion number for this record
  1032  	r, err := g.GetVetted(token, "")
  1033  	if err != nil {
  1034  		return "", fmt.Errorf("GetVetted %v version 0: %v", token, err)
  1035  	}
  1036  
  1037  	// Prepare reply
  1038  	var vrr cmsplugin.VoteResultsReply
  1039  	var vsr cmsplugin.VoteSummaryReply
  1040  	var svr cmsplugin.StartVoteReply
  1041  	vors := make([]cmsplugin.VoteOptionResult, 0,
  1042  		len(vrr.StartVote.Vote.Options))
  1043  
  1044  	// Fill out cast votes
  1045  	vrr.CastVotes, err = g.tallyDCCVotes(vs.Token)
  1046  	if err != nil {
  1047  		return "", fmt.Errorf("Could not tally votes: %v", err)
  1048  	}
  1049  
  1050  	for _, v := range r.Metadata {
  1051  		switch v.ID {
  1052  		case cmsplugin.MDStreamVoteBits:
  1053  			// Start vote
  1054  			sv, err := cmsplugin.DecodeStartVote([]byte(v.Payload))
  1055  			if err != nil {
  1056  				return "", err
  1057  			}
  1058  			vrr.StartVote = sv
  1059  		case cmsplugin.MDStreamVoteSnapshot:
  1060  			svr, err = cmsplugin.DecodeStartVoteReply([]byte(v.Payload))
  1061  			if err != nil {
  1062  				return "", err
  1063  			}
  1064  		}
  1065  	}
  1066  
  1067  	vsr.EndHeight = svr.EndHeight
  1068  	vsr.Duration = vrr.StartVote.Vote.Duration
  1069  	vsr.PassPercentage = vrr.StartVote.Vote.PassPercentage
  1070  
  1071  	for _, voteOption := range vrr.StartVote.Vote.Options {
  1072  		vors = append(vors, cmsplugin.VoteOptionResult{
  1073  			ID:          voteOption.Id,
  1074  			Description: voteOption.Description,
  1075  			Bits:        voteOption.Bits,
  1076  		})
  1077  	}
  1078  
  1079  	for _, vote := range vrr.CastVotes {
  1080  		b, err := strconv.ParseUint(vote.VoteBit, 16, 64)
  1081  		if err != nil {
  1082  			log.Errorf("unable to parse vote bits for vote %v %v",
  1083  				vote.Signature, err)
  1084  			continue
  1085  		}
  1086  		for i, option := range vors {
  1087  			if b == option.Bits {
  1088  				vors[i].Votes++
  1089  			}
  1090  		}
  1091  	}
  1092  	vsr.Results = vors
  1093  
  1094  	reply, err := cmsplugin.EncodeVoteSummaryReply(vsr)
  1095  	if err != nil {
  1096  		return "", fmt.Errorf("Could not encode VoteResultsReply: %v",
  1097  			err)
  1098  	}
  1099  
  1100  	return string(reply), nil
  1101  }
  1102  
  1103  // pluginDCCVoteResults tallies all votes for a dcc. We can run the tally
  1104  // unlocked and just replay the journal. If the replay becomes an issue we
  1105  // could cache it. The Vote that is returned does have to be locked.
  1106  func (g *gitBackEnd) pluginDCCVoteResults(payload string) (string, error) {
  1107  	log.Tracef("pluginDCCVoteResults: %v", payload)
  1108  
  1109  	vote, err := cmsplugin.DecodeVoteResults([]byte(payload))
  1110  	if err != nil {
  1111  		return "", fmt.Errorf("DecodeVoteResults %v", err)
  1112  	}
  1113  
  1114  	// Verify dcc exists, we can run this lockless
  1115  	if !g.vettedPropExists(vote.Token) {
  1116  		return "", fmt.Errorf("dcc not found: %v", vote.Token)
  1117  	}
  1118  
  1119  	// Prepare reply
  1120  	var vrr cmsplugin.VoteResultsReply
  1121  
  1122  	token, err := hex.DecodeString(vote.Token)
  1123  	if err != nil {
  1124  		return "", err
  1125  	}
  1126  
  1127  	// Find the most recent vesion number for this record
  1128  	r, err := g.GetVetted(token, "")
  1129  	if err != nil {
  1130  		return "", fmt.Errorf("GetVetted %v version 0: %v", token, err)
  1131  	}
  1132  
  1133  	for _, v := range r.Metadata {
  1134  		switch v.ID {
  1135  		case cmsplugin.MDStreamVoteBits:
  1136  			// Start vote
  1137  			sv, err := cmsplugin.DecodeStartVote([]byte(v.Payload))
  1138  			if err != nil {
  1139  				return "", err
  1140  			}
  1141  			vrr.StartVote = sv
  1142  		}
  1143  	}
  1144  
  1145  	// Fill out cast votes
  1146  	vrr.CastVotes, err = g.tallyDCCVotes(vote.Token)
  1147  	if err != nil {
  1148  		return "", fmt.Errorf("Could not tally votes: %v", err)
  1149  	}
  1150  
  1151  	reply, err := cmsplugin.EncodeVoteResultsReply(vrr)
  1152  	if err != nil {
  1153  		return "", fmt.Errorf("Could not encode VoteResultsReply: %v",
  1154  			err)
  1155  	}
  1156  
  1157  	return string(reply), nil
  1158  }
  1159  
  1160  // pluginCMSInventory returns the cms plugin inventory for all dccs.  The
  1161  // inventory consists vote details, and cast votes.
  1162  func (g *gitBackEnd) pluginCMSInventory() (string, error) {
  1163  	log.Tracef("pluginInventory")
  1164  
  1165  	g.Lock()
  1166  	defer g.Unlock()
  1167  
  1168  	// Ensure journal has been replayed
  1169  	if !journalsReplayed {
  1170  		return "", backend.ErrJournalsNotReplayed
  1171  	}
  1172  
  1173  	// Walk vetted repo and compile all file paths
  1174  	paths := make([]string, 0, 2048) // PNOOMA
  1175  	err := filepath.Walk(g.vetted,
  1176  		func(path string, info os.FileInfo, err error) error {
  1177  			if err != nil {
  1178  				return err
  1179  			}
  1180  			paths = append(paths, path)
  1181  			return nil
  1182  		})
  1183  	if err != nil {
  1184  		return "", fmt.Errorf("walk vetted: %v", err)
  1185  	}
  1186  
  1187  	// Filter out the file paths for authorize vote metadata and
  1188  	// start vote metadata
  1189  	svPaths := make([]string, 0, len(paths))
  1190  	svFile := fmt.Sprintf("%02v%v", cmsplugin.MDStreamVoteBits,
  1191  		defaultMDFilenameSuffix)
  1192  	for _, v := range paths {
  1193  		switch filepath.Base(v) {
  1194  		case svFile:
  1195  			svPaths = append(svPaths, v)
  1196  		}
  1197  	}
  1198  
  1199  	// Compile the start vote tuples. The in-memory caches that
  1200  	// contain the vote bits and the vote snapshots are lazy
  1201  	// loaded so we have to read vote metadata directly from disk.
  1202  	svt := make([]cmsplugin.StartVoteTuple, 0, len(cmsPluginVoteCache))
  1203  	for _, v := range svPaths {
  1204  		// Read vote bits file into memory
  1205  		b, err := os.ReadFile(v)
  1206  		if err != nil {
  1207  			return "", fmt.Errorf("ReadFile %v: %v", v, err)
  1208  		}
  1209  
  1210  		// Decode vote bits
  1211  		sv, err := cmsplugin.DecodeStartVote(b)
  1212  		if err != nil {
  1213  			return "", fmt.Errorf("DecodeStartVote: %v", err)
  1214  		}
  1215  
  1216  		// Read vote snapshot file into memory
  1217  		dir := filepath.Dir(v)
  1218  		filename := fmt.Sprintf("%02v%v", cmsplugin.MDStreamVoteSnapshot,
  1219  			defaultMDFilenameSuffix)
  1220  		path := filepath.Join(dir, filename)
  1221  		b, err = os.ReadFile(path)
  1222  		if err != nil {
  1223  			return "", fmt.Errorf("ReadFile %v: %v", path, err)
  1224  		}
  1225  
  1226  		// Decode vote snapshot
  1227  		svr, err := cmsplugin.DecodeStartVoteReply(b)
  1228  		if err != nil {
  1229  			return "", fmt.Errorf("DecodeStartVoteReply: %v", err)
  1230  		}
  1231  
  1232  		// Create start vote tuple
  1233  		svt = append(svt, cmsplugin.StartVoteTuple{
  1234  			StartVote:      sv,
  1235  			StartVoteReply: svr,
  1236  		})
  1237  	}
  1238  
  1239  	// Compile cast votes. The in-memory votes cache does not
  1240  	// store the full cast vote struct so we need to replay the
  1241  	// vote journals.
  1242  
  1243  	// Walk journals directory and tally votes for all ballot
  1244  	// journals that are found.
  1245  	cv := make([][]cmsplugin.CastVote, 0, len(svt))
  1246  	err = filepath.Walk(g.journals,
  1247  		func(path string, info os.FileInfo, err error) error {
  1248  			if err != nil {
  1249  				return err
  1250  			}
  1251  
  1252  			if info.Name() == defaultBallotFilename {
  1253  				token := filepath.Base(filepath.Dir(path))
  1254  				votes, err := g.tallyDCCVotes(token)
  1255  				if err != nil {
  1256  					return fmt.Errorf("tallyDCCVotes %v: %v", token, err)
  1257  				}
  1258  
  1259  				cv = append(cv, votes)
  1260  			}
  1261  
  1262  			return nil
  1263  		})
  1264  	if err != nil {
  1265  		return "", fmt.Errorf("walk journals: %v", err)
  1266  	}
  1267  
  1268  	var count = 0
  1269  	for _, v := range cv {
  1270  		count += len(v)
  1271  	}
  1272  	votes := make([]cmsplugin.CastVote, 0, count)
  1273  	for _, v := range cv {
  1274  		votes = append(votes, v...)
  1275  	}
  1276  
  1277  	// Prepare reply
  1278  	ir := cmsplugin.InventoryReply{
  1279  		StartVoteTuples: svt,
  1280  		CastVotes:       votes,
  1281  	}
  1282  
  1283  	payload, err := cmsplugin.EncodeInventoryReply(ir)
  1284  	if err != nil {
  1285  		return "", fmt.Errorf("EncodeInventoryReply: %v", err)
  1286  	}
  1287  
  1288  	return string(payload), nil
  1289  }