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

     1  // Copyright (c) 2017-2019 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  	"bufio"
     9  	"bytes"
    10  	"encoding/base64"
    11  	"encoding/hex"
    12  	"encoding/json"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"net/http"
    17  	"os"
    18  	"path/filepath"
    19  	"strconv"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/decred/dcrd/chaincfg/chainhash"
    24  	"github.com/decred/dcrd/dcrec/secp256k1/v3/ecdsa"
    25  	"github.com/decred/dcrd/dcrutil/v3"
    26  	"github.com/decred/dcrd/wire"
    27  	dcrdataapi "github.com/decred/dcrdata/v6/api/types"
    28  	"github.com/decred/politeia/politeiad/api/v1/identity"
    29  	"github.com/decred/politeia/politeiad/backend"
    30  	"github.com/decred/politeia/politeiad/backend/gitbe/decredplugin"
    31  	"github.com/decred/politeia/util"
    32  )
    33  
    34  // XXX plugins really need to become an interface. Run with this for now.
    35  
    36  const (
    37  	decredPluginIdentity = "fullidentity"
    38  	decredPluginJournals = "journals"
    39  
    40  	defaultCommentIDFilename = "commentid.txt"
    41  	defaultCommentFilename   = "comments.journal"
    42  	defaultCommentsFlushed   = "comments.flushed"
    43  
    44  	defaultBallotFilename = "ballot.journal"
    45  	defaultBallotFlushed  = "ballot.flushed"
    46  
    47  	journalVersion       = "1"       // Version 1 of the comment journal
    48  	journalActionAdd     = "add"     // Add entry
    49  	journalActionDel     = "del"     // Delete entry
    50  	journalActionAddLike = "addlike" // Add comment like
    51  
    52  	flushRecordVersion = "1" // Version 1 of the flush journal
    53  
    54  	// Following are what should be well-known interface hooks
    55  	PluginPostHookEdit = "postedit" // Hook Post Edit
    56  )
    57  
    58  var (
    59  	// errDuplicateVote is emitted when a cast vote is a duplicate.
    60  	errDuplicateVote = errors.New("duplicate vote")
    61  )
    62  
    63  // FlushRecord is a structure that is stored on disk when a journal has been
    64  // flushed.
    65  type FlushRecord struct {
    66  	Version   string `json:"version"`   // Version
    67  	Timestamp string `json:"timestamp"` // Timestamp
    68  }
    69  
    70  // JournalAction prefixes and determines what the next structure is in
    71  // the JSON journal.
    72  // Version is used to determine what version of the comment journal structure
    73  // follows.
    74  // journalActionAdd -> Add entry
    75  // journalActionDel -> Delete entry
    76  // journalActionAddLike -> Add comment like structure (comments only)
    77  type JournalAction struct {
    78  	Version string `json:"version"` // Version
    79  	Action  string `json:"action"`  // Add/Del
    80  }
    81  
    82  var (
    83  	decredPluginSettings map[string]string             // [key]setting
    84  	decredPluginHooks    map[string]func(string) error // [key]func(token) error
    85  
    86  	// Pregenerated journal actions
    87  	journalAdd     []byte
    88  	journalDel     []byte
    89  	journalAddLike []byte
    90  
    91  	// Plugin specific data that CANNOT be treated as metadata
    92  	pluginDataDir = filepath.Join("plugins", "decred")
    93  
    94  	decredPluginCommentsCache = make(map[string]map[string]decredplugin.Comment) // [token][commentid]comment
    95  
    96  	journalsReplayed bool = false
    97  )
    98  
    99  // init is used to pregenerate the JSON journal actions.
   100  func init() {
   101  	var err error
   102  
   103  	journalAdd, err = json.Marshal(JournalAction{
   104  		Version: journalVersion,
   105  		Action:  journalActionAdd,
   106  	})
   107  	if err != nil {
   108  		panic(err.Error())
   109  	}
   110  	journalDel, err = json.Marshal(JournalAction{
   111  		Version: journalVersion,
   112  		Action:  journalActionDel,
   113  	})
   114  	if err != nil {
   115  		panic(err.Error())
   116  	}
   117  	journalAddLike, err = json.Marshal(JournalAction{
   118  		Version: journalVersion,
   119  		Action:  journalActionAddLike,
   120  	})
   121  	if err != nil {
   122  		panic(err.Error())
   123  	}
   124  }
   125  
   126  func getDecredPlugin(dcrdataHost string) backend.Plugin {
   127  	decredPlugin := backend.Plugin{
   128  		ID:       decredplugin.ID,
   129  		Version:  decredplugin.Version,
   130  		Settings: []backend.PluginSetting{},
   131  	}
   132  
   133  	decredPlugin.Settings = append(decredPlugin.Settings,
   134  		backend.PluginSetting{
   135  			Key:   "dcrdata",
   136  			Value: dcrdataHost,
   137  		},
   138  	)
   139  
   140  	// Initialize hooks
   141  	decredPluginHooks = make(map[string]func(string) error)
   142  
   143  	// Initialize settings map
   144  	decredPluginSettings = make(map[string]string)
   145  	for _, v := range decredPlugin.Settings {
   146  		decredPluginSettings[v.Key] = v.Value
   147  	}
   148  	return decredPlugin
   149  }
   150  
   151  // initDecredPlugin is called externally to run initial procedures
   152  // such as replaying journals
   153  func (g *gitBackEnd) initDecredPluginJournals() error {
   154  	log.Infof("initDecredPlugin")
   155  
   156  	// check if backend journal is initialized
   157  	if g.journal == nil {
   158  		return fmt.Errorf("initDecredPlugin backend journal isn't initialized")
   159  	}
   160  
   161  	err := g.replayAllJournals()
   162  	if err != nil {
   163  		log.Infof("initDecredPlugin replay all journals %v", err)
   164  	}
   165  	return nil
   166  }
   167  
   168  // replayAllJournals replays ballot and comment journals for every stored proposal
   169  // this function can be called without the lock held
   170  func (g *gitBackEnd) replayAllJournals() error {
   171  	log.Infof("replayAllJournals")
   172  	files, err := os.ReadDir(g.journals)
   173  	if err != nil {
   174  		return fmt.Errorf("Read dir journals: %v", err)
   175  	}
   176  	for _, f := range files {
   177  		name := f.Name()
   178  		// replay comments for all props
   179  		_, err = g.replayComments(name)
   180  		if err != nil {
   181  			return fmt.Errorf("replayAllJournals replayComments %s %v", name, err)
   182  		}
   183  	}
   184  	journalsReplayed = true
   185  	return nil
   186  }
   187  
   188  // SetDecredPluginSetting removes a setting if the value is "" and adds a setting otherwise.
   189  func setDecredPluginSetting(key, value string) {
   190  	if value == "" {
   191  		delete(decredPluginSettings, key)
   192  		return
   193  	}
   194  
   195  	decredPluginSettings[key] = value
   196  }
   197  
   198  func setDecredPluginHook(name string, f func(string) error) {
   199  	decredPluginHooks[name] = f
   200  }
   201  
   202  func (g *gitBackEnd) vettedPropExists(token string) bool {
   203  	tokenb, err := util.ConvertStringToken(token)
   204  	if err != nil {
   205  		return false
   206  	}
   207  	return g.VettedExists(tokenb)
   208  }
   209  
   210  func (g *gitBackEnd) getNewCid(token string) (string, error) {
   211  	dir := pijoin(g.journals, token)
   212  	err := os.MkdirAll(dir, 0774)
   213  	if err != nil {
   214  		return "", err
   215  	}
   216  
   217  	filename := pijoin(dir, defaultCommentIDFilename)
   218  
   219  	g.Lock()
   220  	defer g.Unlock()
   221  
   222  	fh, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0664)
   223  	if err != nil {
   224  		return "", err
   225  	}
   226  	defer fh.Close()
   227  
   228  	// Determine if file is empty
   229  	fi, err := fh.Stat()
   230  	if err != nil {
   231  		return "", err
   232  	}
   233  	if fi.Size() == 0 {
   234  		// First comment id
   235  		_, err := fmt.Fprintf(fh, "1\n")
   236  		if err != nil {
   237  			return "", err
   238  		}
   239  		return "1", nil
   240  	}
   241  
   242  	// Only allow one line
   243  	var cid string
   244  	s := bufio.NewScanner(fh)
   245  	for i := 0; s.Scan(); i++ {
   246  		if i != 0 {
   247  			return "", fmt.Errorf("comment id file corrupt")
   248  		}
   249  
   250  		c, err := strconv.ParseUint(s.Text(), 10, 64)
   251  		if err != nil {
   252  			return "", err
   253  		}
   254  
   255  		// Increment comment id
   256  		c++
   257  		cid = strconv.FormatUint(c, 10)
   258  
   259  		// Write back new comment id
   260  		_, err = fh.Seek(0, io.SeekStart)
   261  		if err != nil {
   262  			return "", err
   263  		}
   264  		_, err = fmt.Fprintf(fh, "%v\n", c)
   265  		if err != nil {
   266  			return "", err
   267  		}
   268  	}
   269  	if err := s.Err(); err != nil {
   270  		return "", err
   271  	}
   272  
   273  	return cid, nil
   274  }
   275  
   276  // verifyMessage verifies a message is properly signed.
   277  // Copied from https://github.com/decred/dcrd/blob/0fc55252f912756c23e641839b1001c21442c38a/rpcserver.go#L5605
   278  func (g *gitBackEnd) verifyMessage(address, message, signature string) (bool, error) {
   279  	// Decode the provided address.
   280  	addr, err := dcrutil.DecodeAddress(address, g.activeNetParams)
   281  	if err != nil {
   282  		return false, fmt.Errorf("Could not decode address: %v",
   283  			err)
   284  	}
   285  
   286  	// Only P2PKH addresses are valid for signing.
   287  	if _, ok := addr.(*dcrutil.AddressPubKeyHash); !ok {
   288  		return false, fmt.Errorf("Address is not a pay-to-pubkey-hash "+
   289  			"address: %v", address)
   290  	}
   291  
   292  	// Decode base64 signature.
   293  	sig, err := base64.StdEncoding.DecodeString(signature)
   294  	if err != nil {
   295  		return false, fmt.Errorf("Malformed base64 encoding: %v", err)
   296  	}
   297  
   298  	// Validate the signature - this just shows that it was valid at all.
   299  	// we will compare it with the key next.
   300  	var buf bytes.Buffer
   301  	wire.WriteVarString(&buf, 0, "Decred Signed Message:\n")
   302  	wire.WriteVarString(&buf, 0, message)
   303  	expectedMessageHash := chainhash.HashB(buf.Bytes())
   304  	pk, wasCompressed, err := ecdsa.RecoverCompact(sig,
   305  		expectedMessageHash)
   306  	if err != nil {
   307  		// Mirror Bitcoin Core behavior, which treats error in
   308  		// RecoverCompact as invalid signature.
   309  		return false, nil
   310  	}
   311  
   312  	// Reconstruct the pubkey hash.
   313  	dcrPK := pk
   314  	var serializedPK []byte
   315  	if wasCompressed {
   316  		serializedPK = dcrPK.SerializeCompressed()
   317  	} else {
   318  		serializedPK = dcrPK.SerializeUncompressed()
   319  	}
   320  	a, err := dcrutil.NewAddressSecpPubKey(serializedPK, g.activeNetParams)
   321  	if err != nil {
   322  		// Again mirror Bitcoin Core behavior, which treats error in
   323  		// public key reconstruction as invalid signature.
   324  		return false, nil
   325  	}
   326  
   327  	// Return boolean if addresses match.
   328  	return a.Address() == address, nil
   329  }
   330  
   331  func bestBlock() (*dcrdataapi.BlockDataBasic, error) {
   332  	url := decredPluginSettings["dcrdata"] + "/api/block/best"
   333  	log.Debugf("connecting to %v", url)
   334  	// XXX this http command needs a reasonable timeout.
   335  	r, err := http.Get(url)
   336  	log.Debugf("http connecting to %v", url)
   337  	if err != nil {
   338  		return nil, err
   339  	}
   340  	defer r.Body.Close()
   341  
   342  	if r.StatusCode != http.StatusOK {
   343  		body, err := io.ReadAll(r.Body)
   344  		if err != nil {
   345  			return nil, fmt.Errorf("dcrdata error: %v %v %v",
   346  				r.StatusCode, url, err)
   347  		}
   348  		return nil, fmt.Errorf("dcrdata error: %v %v %s",
   349  			r.StatusCode, url, body)
   350  	}
   351  
   352  	var bdb dcrdataapi.BlockDataBasic
   353  	decoder := json.NewDecoder(r.Body)
   354  	if err := decoder.Decode(&bdb); err != nil {
   355  		return nil, err
   356  	}
   357  
   358  	return &bdb, nil
   359  }
   360  
   361  func block(block uint32) (*dcrdataapi.BlockDataBasic, error) {
   362  	h := strconv.FormatUint(uint64(block), 10)
   363  	url := decredPluginSettings["dcrdata"] + "/api/block/" + h
   364  	log.Debugf("connecting to %v", url)
   365  	r, err := http.Get(url)
   366  	if err != nil {
   367  		return nil, err
   368  	}
   369  	defer r.Body.Close()
   370  
   371  	if r.StatusCode != http.StatusOK {
   372  		body, err := io.ReadAll(r.Body)
   373  		if err != nil {
   374  			return nil, fmt.Errorf("dcrdata error: %v %v %v",
   375  				r.StatusCode, url, err)
   376  		}
   377  		return nil, fmt.Errorf("dcrdata error: %v %v %s",
   378  			r.StatusCode, url, body)
   379  	}
   380  
   381  	var bdb dcrdataapi.BlockDataBasic
   382  	decoder := json.NewDecoder(r.Body)
   383  	if err := decoder.Decode(&bdb); err != nil {
   384  		return nil, err
   385  	}
   386  
   387  	return &bdb, nil
   388  }
   389  
   390  // pluginBestBlock returns current best block height from wallet.
   391  func (g *gitBackEnd) pluginBestBlock() (string, error) {
   392  	bb, err := bestBlock()
   393  	if err != nil {
   394  		return "", err
   395  	}
   396  	return strconv.FormatUint(uint64(bb.Height), 10), nil
   397  }
   398  
   399  // decredPluginPostEdit called after and edit is complete but before commit.
   400  func (g *gitBackEnd) decredPluginPostEdit(token string) error {
   401  	log.Tracef("decredPluginPostEdit: %v", token)
   402  
   403  	// The post edit hook gets called on both unvetted and vetted
   404  	// proposals, but comments can only be made on vetted proposals.
   405  	var destination string
   406  	var err error
   407  	if g.vettedPropExists(token) {
   408  		destination, err = g.flushComments(token)
   409  		if err != nil {
   410  			return err
   411  		}
   412  	}
   413  
   414  	// When destination is empty there was nothing to do
   415  	if destination == "" {
   416  		log.Tracef("decredPluginPostEdit: nothing to do %v", token)
   417  		return nil
   418  	}
   419  
   420  	// Add comments to git
   421  	return g.gitAdd(g.unvetted, destination)
   422  }
   423  
   424  // createFlushFile creates a file that indicates that the a journal was flused.
   425  //
   426  // Must be called WITH the mutex held.
   427  func createFlushFile(filename string) error {
   428  	// Mark directory as flushed
   429  	f, err := os.Create(filename)
   430  	if err != nil {
   431  		return err
   432  	}
   433  
   434  	defer f.Close()
   435  
   436  	// Stuff timestamp in flushfile
   437  	j := json.NewEncoder(f)
   438  	err = j.Encode(FlushRecord{
   439  		Version:   flushRecordVersion,
   440  		Timestamp: strconv.FormatInt(time.Now().Unix(), 10),
   441  	})
   442  
   443  	return err
   444  }
   445  
   446  // flushJournalsUnwind unwinds all the flushing action if something goes wrong.
   447  //
   448  // Must be called WITH the mutex held.
   449  func (g *gitBackEnd) flushJournalsUnwind(id string) error {
   450  	// git stash, can fail if there are no uncommitted failures
   451  	err := g.gitStash(g.unvetted)
   452  	if err == nil {
   453  		// git stash drop, allowed to fail
   454  		_ = g.gitStashDrop(g.unvetted)
   455  	}
   456  
   457  	// git checkout master
   458  	err = g.gitCheckout(g.unvetted, "master")
   459  	if err != nil {
   460  		return err
   461  	}
   462  	//  delete branch
   463  	err = g.gitBranchDelete(g.unvetted, id)
   464  	if err != nil {
   465  		return err
   466  	}
   467  	// git clean -xdf
   468  	return g.gitClean(g.unvetted)
   469  }
   470  
   471  // flushCommentflushes comments journal to decred plugin directory in
   472  // git. It returns the filename that was coppied into git repo.
   473  //
   474  // Must be called WITH the mutex held.
   475  func (g *gitBackEnd) flushComments(token string) (string, error) {
   476  	if !g.vettedPropExists(token) {
   477  		return "", fmt.Errorf("unknown proposal: %v", token)
   478  	}
   479  
   480  	// Setup source filenames and verify they actually exist
   481  	srcDir := pijoin(g.journals, token)
   482  	srcComments := pijoin(srcDir, defaultCommentFilename)
   483  	if !util.FileExists(srcComments) {
   484  		return "", nil
   485  	}
   486  
   487  	// Setup destination filenames
   488  	version, err := getLatest(pijoin(g.unvetted, token))
   489  	if err != nil {
   490  		return "", err
   491  	}
   492  	dir := pijoin(g.unvetted, token, version, pluginDataDir)
   493  	comments := pijoin(dir, defaultCommentFilename)
   494  
   495  	// Create the destination container dir
   496  	_ = os.MkdirAll(dir, 0764)
   497  
   498  	// Move journal and comment id into place
   499  	err = g.journal.Copy(srcComments, comments)
   500  	if err != nil {
   501  		return "", err
   502  	}
   503  
   504  	// Return filename that is relative to git dir.
   505  	return pijoin(token, version, pluginDataDir, defaultCommentFilename),
   506  		nil
   507  }
   508  
   509  // flushCommentJournal flushes an individual comment journal.
   510  //
   511  // Must be called WITH the mutex held.
   512  func (g *gitBackEnd) flushCommentJournal(token string) (string, error) {
   513  	// We simply copy the journal into git
   514  	destination, err := g.flushComments(token)
   515  	if err != nil {
   516  		return "", fmt.Errorf("Could not flush %v: %v", token, err)
   517  	}
   518  
   519  	// Create flush record
   520  	filename := pijoin(g.journals, token, defaultCommentsFlushed)
   521  	err = createFlushFile(filename)
   522  	if err != nil {
   523  		return "", fmt.Errorf("Could not mark flushed %v: %v", token,
   524  			err)
   525  	}
   526  
   527  	return destination, nil
   528  }
   529  
   530  // _flushCommentJournals walks all comment journal directories and copies
   531  // modified journals into the unvetted repo. It returns an array of filenames
   532  // that need to be added to the git repo and subsequently rebased into the
   533  // vetted repo .
   534  //
   535  // Must be called WITH the mutex held.
   536  func (g *gitBackEnd) _flushCommentJournals() ([]string, error) {
   537  	dirs, err := os.ReadDir(g.journals)
   538  	if err != nil {
   539  		return nil, err
   540  	}
   541  
   542  	files := make([]string, 0, len(dirs))
   543  	for _, v := range dirs {
   544  		filename := pijoin(g.journals, v.Name(),
   545  			defaultCommentsFlushed)
   546  		log.Tracef("Checking: %v", v.Name())
   547  		if util.FileExists(filename) {
   548  			continue
   549  		}
   550  
   551  		log.Infof("Flushing comments: %v", v.Name())
   552  
   553  		// Add filename to work
   554  		destination, err := g.flushCommentJournal(v.Name())
   555  		if err != nil {
   556  			log.Error(err)
   557  			continue
   558  		}
   559  
   560  		files = append(files, destination)
   561  	}
   562  
   563  	return files, nil
   564  }
   565  
   566  // flushCommentJournals wraps _flushCommentJournals in git magic to revert
   567  // flush in case of errors.
   568  //
   569  // Must be called WITHOUT the mutex held.
   570  func (g *gitBackEnd) flushCommentJournals() error {
   571  	log.Tracef("flushCommentJournals")
   572  
   573  	// We may have to make this more granular
   574  	g.Lock()
   575  	defer g.Unlock()
   576  
   577  	// git checkout master
   578  	err := g.gitCheckout(g.unvetted, "master")
   579  	if err != nil {
   580  		return err
   581  	}
   582  
   583  	// git pull --ff-only --rebase
   584  	err = g.gitPull(g.unvetted, true)
   585  	if err != nil {
   586  		return err
   587  	}
   588  
   589  	// git checkout -b timestamp_flushcomments
   590  	branch := strconv.FormatInt(time.Now().Unix(), 10) + "_flushcomments"
   591  	_ = g.gitBranchDelete(g.unvetted, branch) // Just in case
   592  	err = g.gitNewBranch(g.unvetted, branch)
   593  	if err != nil {
   594  		return err
   595  	}
   596  
   597  	// closure to handle unwind if needed
   598  	var errUnwind error
   599  	defer func() {
   600  		if errUnwind == nil {
   601  			return
   602  		}
   603  		err := g.flushJournalsUnwind(branch)
   604  		if err != nil {
   605  			log.Errorf("flushJournalsUnwind: %v", err)
   606  		}
   607  	}()
   608  
   609  	// Flush journals
   610  	files, err := g._flushCommentJournals()
   611  	if err != nil {
   612  		errUnwind = err
   613  		return err
   614  	}
   615  
   616  	if len(files) == 0 {
   617  		log.Info("flushCommentJournals: nothing to do")
   618  		err = g.flushJournalsUnwind(branch)
   619  		if err != nil {
   620  			log.Errorf("flushJournalsUnwind: %v", err)
   621  		}
   622  		return nil
   623  	}
   624  
   625  	// git add journals
   626  	commitMessage := "Flush comment journals.\n\n"
   627  	for _, v := range files {
   628  		err = g.gitAdd(g.unvetted, v)
   629  		if err != nil {
   630  			errUnwind = err
   631  			return err
   632  		}
   633  
   634  		s := strings.Split(v, string(os.PathSeparator))
   635  		if len(s) == 0 {
   636  			commitMessage += "ERROR: " + v + "\n"
   637  		} else {
   638  			commitMessage += s[0] + "\n"
   639  		}
   640  	}
   641  
   642  	// git commit
   643  	err = g.gitCommit(g.unvetted, commitMessage)
   644  	if err != nil {
   645  		errUnwind = err
   646  		return err
   647  	}
   648  
   649  	// git rebase master
   650  	err = g.rebasePR(branch)
   651  	if err != nil {
   652  		errUnwind = err
   653  		return err
   654  	}
   655  
   656  	return nil
   657  }
   658  
   659  // flushVotes flushes votes journal to decred plugin directory in git. It
   660  // returns the filename that was coppied into git repo.
   661  //
   662  // Must be called WITH the mutex held.
   663  func (g *gitBackEnd) flushVotes(token string) (string, error) {
   664  	if !g.vettedPropExists(token) {
   665  		return "", fmt.Errorf("unknown proposal: %v", token)
   666  	}
   667  
   668  	// Setup source filenames and verify they actually exist
   669  	srcDir := pijoin(g.journals, token)
   670  	srcVotes := pijoin(srcDir, defaultBallotFilename)
   671  	if !util.FileExists(srcVotes) {
   672  		return "", nil
   673  	}
   674  
   675  	// Setup destination filenames
   676  	version, err := getLatest(pijoin(g.unvetted, token))
   677  	if err != nil {
   678  		return "", err
   679  	}
   680  	dir := pijoin(g.unvetted, token, version, pluginDataDir)
   681  	votes := pijoin(dir, defaultBallotFilename)
   682  
   683  	// Create the destination container dir
   684  	_ = os.MkdirAll(dir, 0764)
   685  
   686  	// Move journal into place
   687  	err = g.journal.Copy(srcVotes, votes)
   688  	if err != nil {
   689  		return "", err
   690  	}
   691  
   692  	// Return filename that is relative to git dir.
   693  	return pijoin(token, version, pluginDataDir, defaultBallotFilename), nil
   694  }
   695  
   696  // _flushVotesJournals walks all votes journal directories and copies
   697  // modified journals into the unvetted repo. It returns an array of filenames
   698  // that need to be added to the git repo and subsequently rebased into the
   699  // vetted repo .
   700  //
   701  // Must be called WITH the mutex held.
   702  func (g *gitBackEnd) _flushVotesJournals() ([]string, error) {
   703  	dirs, err := os.ReadDir(g.journals)
   704  	if err != nil {
   705  		return nil, err
   706  	}
   707  
   708  	files := make([]string, 0, len(dirs))
   709  	for _, v := range dirs {
   710  		filename := pijoin(g.journals, v.Name(),
   711  			defaultBallotFlushed)
   712  		log.Tracef("Checking: %v", v.Name())
   713  		if util.FileExists(filename) {
   714  			continue
   715  		}
   716  
   717  		log.Infof("Flushing votes: %v", v.Name())
   718  
   719  		// We simply copy the journal into git
   720  		destination, err := g.flushVotes(v.Name())
   721  		if err != nil {
   722  			log.Errorf("Could not flush %v: %v", v.Name(), err)
   723  			continue
   724  		}
   725  
   726  		// Create flush record
   727  		err = createFlushFile(filename)
   728  		if err != nil {
   729  			log.Errorf("Could not mark flushed %v: %v", v.Name(),
   730  				err)
   731  			continue
   732  		}
   733  
   734  		// Add filename to work
   735  		files = append(files, destination)
   736  	}
   737  
   738  	return files, nil
   739  }
   740  
   741  // flushVoteJournals wraps _flushVoteJournals in git magic to revert
   742  // flush in case of errors.
   743  //
   744  // Must be called WITHOUT the mutex held.
   745  func (g *gitBackEnd) flushVoteJournals() error {
   746  	log.Tracef("flushVoteJournals")
   747  
   748  	// We may have to make this more granular
   749  	g.Lock()
   750  	defer g.Unlock()
   751  
   752  	// git checkout master
   753  	err := g.gitCheckout(g.unvetted, "master")
   754  	if err != nil {
   755  		return err
   756  	}
   757  
   758  	// git pull --ff-only --rebase
   759  	err = g.gitPull(g.unvetted, true)
   760  	if err != nil {
   761  		return err
   762  	}
   763  
   764  	// git checkout -b timestamp_flushvotes
   765  	branch := strconv.FormatInt(time.Now().Unix(), 10) + "_flushvotes"
   766  	_ = g.gitBranchDelete(g.unvetted, branch) // Just in case
   767  	err = g.gitNewBranch(g.unvetted, branch)
   768  	if err != nil {
   769  		return err
   770  	}
   771  
   772  	// closure to handle unwind if needed
   773  	var errUnwind error
   774  	defer func() {
   775  		if errUnwind == nil {
   776  			return
   777  		}
   778  		err := g.flushJournalsUnwind(branch)
   779  		if err != nil {
   780  			log.Errorf("flushJournalsUnwind: %v", err)
   781  		}
   782  	}()
   783  
   784  	// Flush journals
   785  	files, err := g._flushVotesJournals()
   786  	if err != nil {
   787  		errUnwind = err
   788  		return err
   789  	}
   790  
   791  	if len(files) == 0 {
   792  		log.Info("flushVotesJournals: nothing to do")
   793  		err = g.flushJournalsUnwind(branch)
   794  		if err != nil {
   795  			log.Errorf("flushJournalsUnwind: %v", err)
   796  		}
   797  		return nil
   798  	}
   799  
   800  	// git add journals
   801  	commitMessage := "Flush vote journals.\n\n"
   802  	for _, v := range files {
   803  		err = g.gitAdd(g.unvetted, v)
   804  		if err != nil {
   805  			errUnwind = err
   806  			return err
   807  		}
   808  
   809  		s := strings.Split(v, string(os.PathSeparator))
   810  		if len(s) == 0 {
   811  			commitMessage += "ERROR: " + v + "\n"
   812  		} else {
   813  			commitMessage += s[0] + "\n"
   814  		}
   815  	}
   816  
   817  	// git commit
   818  	err = g.gitCommit(g.unvetted, commitMessage)
   819  	if err != nil {
   820  		errUnwind = err
   821  		return err
   822  	}
   823  
   824  	// git rebase master
   825  	err = g.rebasePR(branch)
   826  	if err != nil {
   827  		errUnwind = err
   828  		return err
   829  	}
   830  
   831  	return nil
   832  }
   833  func (g *gitBackEnd) decredPluginJournalFlusher() {
   834  	// XXX make this a single PR instead of 2 to save some git time
   835  	err := g.flushCommentJournals()
   836  	if err != nil {
   837  		log.Errorf("decredPluginJournalFlusher: %v", err)
   838  	}
   839  	err = g.flushVoteJournals()
   840  	if err != nil {
   841  		log.Errorf("decredPluginVoteFlusher: %v", err)
   842  	}
   843  }
   844  
   845  func (g *gitBackEnd) pluginNewComment(payload string) (string, error) {
   846  	// XXX this should become part of some sort of context
   847  	fiJSON, ok := decredPluginSettings[decredPluginIdentity]
   848  	if !ok {
   849  		return "", fmt.Errorf("full identity not set")
   850  	}
   851  	fi, err := identity.UnmarshalFullIdentity([]byte(fiJSON))
   852  	if err != nil {
   853  		return "", err
   854  	}
   855  
   856  	// Decode comment
   857  	comment, err := decredplugin.DecodeNewComment([]byte(payload))
   858  	if err != nil {
   859  		return "", fmt.Errorf("DecodeNewComment: %v", err)
   860  	}
   861  
   862  	// Verify proposal exists, we can run this lockless
   863  	if !g.vettedPropExists(comment.Token) {
   864  		return "", fmt.Errorf("unknown proposal: %v", comment.Token)
   865  	}
   866  
   867  	// Do some cheap things before expensive calls
   868  	cfilename := pijoin(g.journals, comment.Token,
   869  		defaultCommentFilename)
   870  	if comment.ParentID == "" {
   871  		// Empty ParentID means comment 0
   872  		comment.ParentID = "0"
   873  	}
   874  
   875  	// Sign signature
   876  	r := fi.SignMessage([]byte(comment.Signature))
   877  	receipt := hex.EncodeToString(r[:])
   878  
   879  	// Create new comment id
   880  	cid, err := g.getNewCid(comment.Token)
   881  	if err != nil {
   882  		return "", fmt.Errorf("could not generate new comment id: %v",
   883  			err)
   884  	}
   885  
   886  	// Create Journal entry
   887  	c := decredplugin.Comment{
   888  		Token:     comment.Token,
   889  		ParentID:  comment.ParentID,
   890  		Comment:   comment.Comment,
   891  		Signature: comment.Signature,
   892  		PublicKey: comment.PublicKey,
   893  		CommentID: cid,
   894  		Receipt:   receipt,
   895  		Timestamp: time.Now().Unix(),
   896  	}
   897  	blob, err := decredplugin.EncodeComment(c)
   898  	if err != nil {
   899  		return "", fmt.Errorf("EncodeComment: %v", err)
   900  	}
   901  
   902  	// Add comment to journal
   903  	err = g.journal.Journal(cfilename, string(journalAdd)+
   904  		string(blob))
   905  	if err != nil {
   906  		return "", fmt.Errorf("could not journal %v: %v", c.Token, err)
   907  	}
   908  
   909  	// Comment journal filename
   910  	flushFilename := pijoin(g.journals, comment.Token,
   911  		defaultCommentsFlushed)
   912  
   913  	// Cache comment
   914  	g.Lock()
   915  
   916  	// Mark comment journal dirty
   917  	_ = os.Remove(flushFilename)
   918  
   919  	// Remove from cash.
   920  	if _, ok := decredPluginCommentsCache[c.Token]; !ok {
   921  		decredPluginCommentsCache[c.Token] =
   922  			make(map[string]decredplugin.Comment)
   923  	}
   924  	_, ok = decredPluginCommentsCache[c.Token][c.CommentID]
   925  	if ok {
   926  		// Sanity
   927  		log.Errorf("comment should not have existed.")
   928  	}
   929  	decredPluginCommentsCache[c.Token][c.CommentID] = c
   930  	g.Unlock()
   931  
   932  	// Encode reply
   933  	ncr := decredplugin.NewCommentReply{
   934  		CommentID: c.CommentID,
   935  		Receipt:   c.Receipt,
   936  		Timestamp: c.Timestamp,
   937  	}
   938  	ncrb, err := decredplugin.EncodeNewCommentReply(ncr)
   939  	if err != nil {
   940  		return "", fmt.Errorf("EncodeNewCommentReply: %v", err)
   941  	}
   942  
   943  	// return success and encoded answer
   944  	return string(ncrb), nil
   945  }
   946  
   947  func (g *gitBackEnd) pluginCensorComment(payload string) (string, error) {
   948  	log.Tracef("pluginCensorComment")
   949  
   950  	// Check if journals were replayed
   951  	if !journalsReplayed {
   952  		return "", backend.ErrJournalsNotReplayed
   953  	}
   954  
   955  	// XXX this should become part of some sort of context
   956  	fiJSON, ok := decredPluginSettings[decredPluginIdentity]
   957  	if !ok {
   958  		return "", fmt.Errorf("full identity not set")
   959  	}
   960  	fi, err := identity.UnmarshalFullIdentity([]byte(fiJSON))
   961  	if err != nil {
   962  		return "", fmt.Errorf("UnmarshalFullIdentity: %v", err)
   963  	}
   964  
   965  	// Decode censor comment
   966  	censor, err := decredplugin.DecodeCensorComment([]byte(payload))
   967  	if err != nil {
   968  		return "", fmt.Errorf("DecodeCensorComment: %v", err)
   969  	}
   970  
   971  	// Verify proposal exists, we can run this lockless
   972  	if !g.vettedPropExists(censor.Token) {
   973  		return "", fmt.Errorf("unknown proposal: %v", censor.Token)
   974  	}
   975  
   976  	// Sign signature
   977  	r := fi.SignMessage([]byte(censor.Signature))
   978  	receipt := hex.EncodeToString(r[:])
   979  
   980  	// Comment journal filename
   981  	flushFilename := pijoin(g.journals, censor.Token,
   982  		defaultCommentsFlushed)
   983  
   984  	// Ensure proposal exists in comments cache
   985  	g.Lock()
   986  
   987  	// Mark comment journal dirty
   988  	_ = os.Remove(flushFilename)
   989  
   990  	// Verify cache
   991  	_, ok = decredPluginCommentsCache[censor.Token]
   992  	if !ok {
   993  		g.Unlock()
   994  		return "", fmt.Errorf("proposal not found %v", censor.Token)
   995  	}
   996  
   997  	// Ensure comment exists in comments cache and has not
   998  	// already been censored
   999  	c, ok := decredPluginCommentsCache[censor.Token][censor.CommentID]
  1000  	if !ok {
  1001  		g.Unlock()
  1002  		return "", fmt.Errorf("comment not found %v:%v",
  1003  			censor.Token, censor.CommentID)
  1004  	}
  1005  	if c.Censored {
  1006  		g.Unlock()
  1007  		return "", fmt.Errorf("comment already censored %v: %v",
  1008  			censor.Token, censor.CommentID)
  1009  	}
  1010  
  1011  	// Update comments cache
  1012  	oc := c
  1013  	c.Comment = ""
  1014  	c.Censored = true
  1015  	decredPluginCommentsCache[censor.Token][censor.CommentID] = c
  1016  
  1017  	g.Unlock()
  1018  
  1019  	// We create an unwind function that MUST be called from all error
  1020  	// paths. If everything works ok it is a no-op.
  1021  	unwind := func() {
  1022  		g.Lock()
  1023  		decredPluginCommentsCache[censor.Token][censor.CommentID] = oc
  1024  		g.Unlock()
  1025  	}
  1026  
  1027  	// Create Journal entry
  1028  	cc := decredplugin.CensorComment{
  1029  		Token:     censor.Token,
  1030  		CommentID: censor.CommentID,
  1031  		Reason:    censor.Reason,
  1032  		Signature: censor.Signature,
  1033  		PublicKey: censor.PublicKey,
  1034  		Receipt:   receipt,
  1035  		Timestamp: time.Now().Unix(),
  1036  	}
  1037  	blob, err := decredplugin.EncodeCensorComment(cc)
  1038  	if err != nil {
  1039  		unwind()
  1040  		return "", fmt.Errorf("EncodeCensorComment: %v", err)
  1041  	}
  1042  
  1043  	// Add censor comment to journal
  1044  	cfilename := pijoin(g.journals, censor.Token,
  1045  		defaultCommentFilename)
  1046  	err = g.journal.Journal(cfilename, string(journalDel)+string(blob))
  1047  	if err != nil {
  1048  		unwind()
  1049  		return "", fmt.Errorf("could not journal %v: %v", cc.Token, err)
  1050  	}
  1051  
  1052  	// Encode reply
  1053  	ccr := decredplugin.CensorCommentReply{
  1054  		Receipt: cc.Receipt,
  1055  	}
  1056  	ccrb, err := decredplugin.EncodeCensorCommentReply(ccr)
  1057  	if err != nil {
  1058  		unwind()
  1059  		return "", fmt.Errorf("EncodeCensorCommentReply: %v", err)
  1060  	}
  1061  
  1062  	return string(ccrb), nil
  1063  }
  1064  
  1065  // encodeGetCommentsReply converts a comment map into a JSON string that can be
  1066  // returned as a decredplugin reply. If the comment map is nil it returns a
  1067  // valid empty reply structure.
  1068  func encodeGetCommentsReply(cm map[string]decredplugin.Comment) (string, error) {
  1069  	if cm == nil {
  1070  		cm = make(map[string]decredplugin.Comment)
  1071  	}
  1072  
  1073  	// Encode reply
  1074  	gcr := decredplugin.GetCommentsReply{
  1075  		Comments: make([]decredplugin.Comment, 0, len(cm)),
  1076  	}
  1077  	for _, v := range cm {
  1078  		gcr.Comments = append(gcr.Comments, v)
  1079  	}
  1080  
  1081  	gcrb, err := decredplugin.EncodeGetCommentsReply(gcr)
  1082  	if err != nil {
  1083  		return "", fmt.Errorf("encodeGetCommentsReply: %v", err)
  1084  	}
  1085  
  1086  	return string(gcrb), nil
  1087  }
  1088  
  1089  // replayComments replay the comments for a given proposal
  1090  // the proposal is matched by the provided token
  1091  // this function can be called WITHOUT the lock held
  1092  func (g *gitBackEnd) replayComments(token string) (map[string]decredplugin.Comment, error) {
  1093  	log.Debugf("replayComments %s", token)
  1094  	// Verify proposal exists, we can run this lockless
  1095  	if !g.vettedPropExists(token) {
  1096  		return nil, nil
  1097  	}
  1098  
  1099  	// Do some cheap things before expensive calls
  1100  	cfilename := pijoin(g.journals, token,
  1101  		defaultCommentFilename)
  1102  
  1103  	// Replay journal
  1104  	err := g.journal.Open(cfilename)
  1105  	if err != nil {
  1106  		if !os.IsNotExist(err) {
  1107  			return nil, fmt.Errorf("journal.Open: %v", err)
  1108  		}
  1109  		return nil, nil
  1110  	}
  1111  	defer func() {
  1112  		err = g.journal.Close(cfilename)
  1113  		if err != nil {
  1114  			log.Errorf("journal.Close: %v", err)
  1115  		}
  1116  	}()
  1117  
  1118  	comments := make(map[string]decredplugin.Comment)
  1119  
  1120  	for {
  1121  		err = g.journal.Replay(cfilename, func(s string) error {
  1122  			ss := bytes.NewReader([]byte(s))
  1123  			d := json.NewDecoder(ss)
  1124  
  1125  			// Decode action
  1126  			var action JournalAction
  1127  			err = d.Decode(&action)
  1128  			if err != nil {
  1129  				return fmt.Errorf("journal action: %v", err)
  1130  			}
  1131  
  1132  			switch action.Action {
  1133  			case journalActionAdd:
  1134  				var c decredplugin.Comment
  1135  				err = d.Decode(&c)
  1136  				if err != nil {
  1137  					return fmt.Errorf("journal add: %v",
  1138  						err)
  1139  				}
  1140  
  1141  				// Sanity
  1142  				if _, ok := comments[c.CommentID]; ok {
  1143  					log.Errorf("duplicate comment id %v",
  1144  						c.CommentID)
  1145  				}
  1146  				comments[c.CommentID] = c
  1147  
  1148  			case journalActionDel:
  1149  				var cc decredplugin.CensorComment
  1150  				err = d.Decode(&cc)
  1151  				if err != nil {
  1152  					return fmt.Errorf("journal censor: %v",
  1153  						err)
  1154  				}
  1155  
  1156  				// Ensure comment has been added
  1157  				c, ok := comments[cc.CommentID]
  1158  				if !ok {
  1159  					// Complain but we can't do anything
  1160  					// about it. Can't return error or we'd
  1161  					// abort journal loop.
  1162  					log.Errorf("comment not found: %v",
  1163  						cc.CommentID)
  1164  					return nil
  1165  				}
  1166  
  1167  				// Delete comment
  1168  				c.Comment = ""
  1169  				c.Censored = true
  1170  				comments[cc.CommentID] = c
  1171  
  1172  			default:
  1173  				return fmt.Errorf("invalid action: %v",
  1174  					action.Action)
  1175  			}
  1176  			return nil
  1177  		})
  1178  		if errors.Is(err, io.EOF) {
  1179  			break
  1180  		} else if err != nil {
  1181  			return nil, err
  1182  		}
  1183  	}
  1184  
  1185  	g.Lock()
  1186  	decredPluginCommentsCache[token] = comments
  1187  	g.Unlock()
  1188  
  1189  	return comments, nil
  1190  }
  1191  
  1192  func (g *gitBackEnd) pluginGetComments(payload string) (string, error) {
  1193  	log.Tracef("pluginGetComments")
  1194  
  1195  	// Check if journals were replayed
  1196  	if !journalsReplayed {
  1197  		return "", backend.ErrJournalsNotReplayed
  1198  	}
  1199  
  1200  	// Decode comment
  1201  	gc, err := decredplugin.DecodeGetComments([]byte(payload))
  1202  	if err != nil {
  1203  		return "", fmt.Errorf("DecodeGetComments: %v", err)
  1204  	}
  1205  
  1206  	g.Lock()
  1207  	comments := decredPluginCommentsCache[gc.Token]
  1208  	g.Unlock()
  1209  	return encodeGetCommentsReply(comments)
  1210  }