github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/tstore/tstore.go (about)

     1  // Copyright (c) 2020-2021 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package tstore
     6  
     7  import (
     8  	"encoding/binary"
     9  	"fmt"
    10  	"net/url"
    11  	"os"
    12  	"path/filepath"
    13  	"sync"
    14  
    15  	"github.com/decred/dcrd/chaincfg/v3"
    16  	backend "github.com/decred/politeia/politeiad/backendv2"
    17  	"github.com/decred/politeia/politeiad/backendv2/tstorebe/store"
    18  	"github.com/decred/politeia/politeiad/backendv2/tstorebe/store/mysql"
    19  	"github.com/decred/politeia/politeiad/backendv2/tstorebe/tlog"
    20  	"github.com/decred/politeia/util"
    21  	"github.com/pkg/errors"
    22  	"github.com/robfig/cron"
    23  )
    24  
    25  const (
    26  	// MySQL settings
    27  	dbUser = "politeiad"
    28  )
    29  
    30  // Tstore is a data store that automatically timestamps all data saved to it
    31  // onto the decred blockchain, making it possible to cryptographically prove
    32  // that a piece of data existed at a specific block height. It combines a
    33  // trillian log (tlog) and a key-value store. When data is saved to a tstore
    34  // instance it is first saved to the key-value store then a digest of the data
    35  // is appended onto the tlog tree. Tlog trees are episodically timestamped onto
    36  // the decred blockchain. An inlcusion proof, i.e. the cryptographic proof that
    37  // the data was included in the decred timestamp, can be retrieved for any
    38  // individual piece of data saved to the tstore.
    39  //
    40  // Saving only the digest of the data to tlog means that we separate the
    41  // timestamp from the data itself. This allows us to remove content that is
    42  // deemed undesirable from the key-value store without impacting the ability to
    43  // retrieve inclusion proofs for any other pieces of data saved to tstore.
    44  //
    45  // The tlog tree is append only and is treated as the source of truth. If any
    46  // blobs make it into the key-value store but do not make it into the tlog tree
    47  // they are considered to be orphaned and are simply ignored. We do not unwind
    48  // failed calls.
    49  type Tstore struct {
    50  	sync.RWMutex
    51  	dataDir         string
    52  	activeNetParams *chaincfg.Params
    53  	tlog            tlog.Client
    54  	store           store.BlobKV
    55  	dcrtime         *dcrtimeClient
    56  	cron            *cron.Cron
    57  	plugins         map[string]plugin // [pluginID]plugin
    58  
    59  	// droppingAnchor indicates whether tstore is in the process of
    60  	// dropping an anchor, i.e. timestamping unanchored tlog trees
    61  	// using dcrtime. An anchor is dropped periodically using cron.
    62  	droppingAnchor bool
    63  
    64  	// tokens contains the short token to full token mappings. The
    65  	// short token is the first n characters of the hex encoded record
    66  	// token, where n is defined by the short token length politeiad
    67  	// setting. Record lookups using short tokens are allowed. This
    68  	// cache is used to prevent collisions when creating new tokens
    69  	// and to facilitate lookups using only the short token. This cache
    70  	// is built on startup.
    71  	tokens map[string][]byte // [shortToken]fullToken
    72  }
    73  
    74  // tokenFromTreeID returns the record token for a tlog tree.
    75  func tokenFromTreeID(treeID int64) []byte {
    76  	b := make([]byte, 8)
    77  	binary.LittleEndian.PutUint64(b, uint64(treeID))
    78  	return b
    79  }
    80  
    81  // treeIDFromToken returns the tlog tree ID for the given record token.
    82  func treeIDFromToken(token []byte) int64 {
    83  	return int64(binary.LittleEndian.Uint64(token))
    84  }
    85  
    86  // tokenIsFullLength returns whether the token is a full length token.
    87  func tokenIsFullLength(token []byte) bool {
    88  	return util.TokenIsFullLength(util.TokenTypeTstore, token)
    89  }
    90  
    91  // tokenCollision returns whether the short version of the provided token
    92  // already exists. This can be used to prevent collisions when creating new
    93  // tokens.
    94  func (t *Tstore) tokenCollision(fullToken []byte) bool {
    95  	shortToken, err := util.ShortTokenEncode(fullToken)
    96  	if err != nil {
    97  		return false
    98  	}
    99  
   100  	t.RLock()
   101  	defer t.RUnlock()
   102  
   103  	_, ok := t.tokens[shortToken]
   104  	return ok
   105  }
   106  
   107  // tokenAdd adds a entry to the tokens cache.
   108  func (t *Tstore) tokenAdd(fullToken []byte) error {
   109  	if !tokenIsFullLength(fullToken) {
   110  		return fmt.Errorf("token is not full length")
   111  	}
   112  
   113  	shortToken, err := util.ShortTokenEncode(fullToken)
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	t.Lock()
   119  	t.tokens[shortToken] = fullToken
   120  	t.Unlock()
   121  
   122  	log.Tracef("Token cache add: %v", shortToken)
   123  
   124  	return nil
   125  }
   126  
   127  // fullLengthToken returns the full length token given the short token. A
   128  // ErrRecordNotFound error is returned if a record does not exist for the
   129  // provided token.
   130  func (t *Tstore) fullLengthToken(token []byte) ([]byte, error) {
   131  	if tokenIsFullLength(token) {
   132  		// Token is already full length. Nothing else to do.
   133  		return token, nil
   134  	}
   135  
   136  	shortToken, err := util.ShortTokenEncode(token)
   137  	if err != nil {
   138  		// Token was not large enough to be a short token. This cannot
   139  		// be used to lookup a record.
   140  		return nil, backend.ErrRecordNotFound
   141  	}
   142  
   143  	t.RLock()
   144  	defer t.RUnlock()
   145  
   146  	fullToken, ok := t.tokens[shortToken]
   147  	if !ok {
   148  		// Short token does not correspond to a record token
   149  		return nil, backend.ErrRecordNotFound
   150  	}
   151  
   152  	return fullToken, nil
   153  }
   154  
   155  // Fsck performs a filesystem check on the tstore.
   156  func (t *Tstore) Fsck(allTokens [][]byte) error {
   157  	err := t.anchorTrees()
   158  	if err != nil {
   159  		return err
   160  	}
   161  	err = t.freezeTreeCheck()
   162  	if err != nil {
   163  		return err
   164  	}
   165  
   166  	// Run the plugin fscks
   167  	for _, pluginID := range t.pluginIDs() {
   168  		p, _ := t.plugin(pluginID)
   169  
   170  		log.Infof("Performing fsck on the %v plugin", pluginID)
   171  
   172  		err := p.client.Fsck(allTokens)
   173  		if err != nil {
   174  			return errors.Errorf("plugin %v fsck: %v",
   175  				pluginID, err)
   176  		}
   177  	}
   178  
   179  	return nil
   180  }
   181  
   182  // Close performs cleanup of the tstore.
   183  func (t *Tstore) Close() {
   184  	log.Tracef("Close")
   185  
   186  	// Close connections
   187  	t.tlog.Close()
   188  	t.store.Close()
   189  }
   190  
   191  // Setup performs any required work to setup the tstore instance.
   192  func (t *Tstore) Setup() error {
   193  	log.Infof("Building backend token prefix cache")
   194  
   195  	tokens, err := t.Inventory()
   196  	if err != nil {
   197  		return fmt.Errorf("Inventory: %v", err)
   198  	}
   199  
   200  	log.Infof("%v records in the tstore", len(tokens))
   201  
   202  	for _, v := range tokens {
   203  		t.tokenAdd(v)
   204  	}
   205  
   206  	return nil
   207  }
   208  
   209  // New returns a new tstore instance.
   210  func New(appDir, dataDir string, anp *chaincfg.Params, tlogHost, dbHost, dbPass, dcrtimeHost, dcrtimeCert string) (*Tstore, error) {
   211  	// Setup datadir for this tstore instance
   212  	dataDir = filepath.Join(dataDir)
   213  	err := os.MkdirAll(dataDir, 0700)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  
   218  	// Setup the key-value store
   219  	//
   220  	// Example db name: testnet3_unvetted_kv
   221  	dbName := fmt.Sprintf("%v_kv", anp.Name)
   222  	kvstore, err := mysql.New(dbHost, dbUser, dbPass, dbName)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  
   227  	// Setup trillian client
   228  	log.Infof("Tlog host: %v", tlogHost)
   229  	tlogClient, err := tlog.NewClient(tlogHost)
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  
   234  	// Verify dcrtime host
   235  	_, err = url.Parse(dcrtimeHost)
   236  	if err != nil {
   237  		return nil, fmt.Errorf("parse dcrtime host '%v': %v", dcrtimeHost, err)
   238  	}
   239  	log.Infof("Anchor host: %v", dcrtimeHost)
   240  
   241  	// Setup dcrtime client
   242  	dcrtimeClient, err := newDcrtimeClient(dcrtimeHost, dcrtimeCert)
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  
   247  	// Setup tstore
   248  	t := Tstore{
   249  		dataDir:         dataDir,
   250  		activeNetParams: anp,
   251  		tlog:            tlogClient,
   252  		store:           kvstore,
   253  		dcrtime:         dcrtimeClient,
   254  		cron:            cron.New(),
   255  		plugins:         make(map[string]plugin),
   256  		tokens:          make(map[string][]byte),
   257  	}
   258  
   259  	// Launch cron
   260  	log.Infof("Launch cron anchor job")
   261  	err = t.cron.AddFunc(anchorSchedule, func() {
   262  		err := t.anchorTrees()
   263  		if err != nil {
   264  			log.Errorf("anchorTrees: %v", err)
   265  		}
   266  		err = t.freezeTreeCheck()
   267  		if err != nil {
   268  			log.Errorf("freeTreeCheck: %v", err)
   269  		}
   270  	})
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  	t.cron.Start()
   275  
   276  	return &t, nil
   277  }