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 }