github.com/deso-protocol/core@v1.2.9/lib/txindex.go (about) 1 package lib 2 3 import ( 4 "encoding/hex" 5 "fmt" 6 "github.com/dgraph-io/badger/v3" 7 "path/filepath" 8 "reflect" 9 "sync" 10 "time" 11 12 chainlib "github.com/btcsuite/btcd/blockchain" 13 "github.com/deso-protocol/go-deadlock" 14 "github.com/golang/glog" 15 ) 16 17 type TXIndex struct { 18 // TXIndexLock protects the transaction index. 19 TXIndexLock deadlock.RWMutex 20 21 // The txindex has it s own separate Blockchain object. This allows us to 22 // capture more metadata when collecting transactions without interfering 23 // with the goings-on of the main chain. 24 TXIndexChain *Blockchain 25 26 // Core objects from Server 27 CoreChain *Blockchain 28 29 // Core params object 30 Params *DeSoParams 31 32 // Update wait group 33 updateWaitGroup sync.WaitGroup 34 35 // Shutdown channel 36 stopUpdateChannel chan struct{} 37 } 38 39 func NewTXIndex(coreChain *Blockchain, params *DeSoParams, dataDirectory string) (*TXIndex, error) { 40 // Initialize database 41 txIndexDir := filepath.Join(GetBadgerDbPath(dataDirectory), "txindex") 42 txIndexOpts := badger.DefaultOptions(txIndexDir) 43 txIndexOpts.ValueDir = GetBadgerDbPath(txIndexDir) 44 txIndexOpts.MemTableSize = 1024 << 20 45 glog.Infof("TxIndex BadgerDB Dir: %v", txIndexOpts.Dir) 46 glog.Infof("TxIndex BadgerDB ValueDir: %v", txIndexOpts.ValueDir) 47 txIndexDb, err := badger.Open(txIndexOpts) 48 if err != nil { 49 glog.Fatal(err) 50 } 51 52 // See if we have a best chain hash stored in the txindex db. 53 bestBlockHashBeforeInit := DbGetBestHash(txIndexDb, ChainTypeDeSoBlock) 54 55 // If we haven't initialized the txIndexChain before, set up the 56 // seed mappings. 57 if bestBlockHashBeforeInit == nil { 58 59 // Add the seed balances. Originate them from the architect public key and 60 // set their block as the genesis block. 61 { 62 dummyPk := ArchitectPubKeyBase58Check 63 dummyTxn := &MsgDeSoTxn{ 64 TxInputs: []*DeSoInput{}, 65 TxOutputs: params.SeedBalances, 66 TxnMeta: &BlockRewardMetadataa{}, 67 PublicKey: MustBase58CheckDecode(dummyPk), 68 } 69 affectedPublicKeys := []*AffectedPublicKey{} 70 totalOutput := uint64(0) 71 for _, seedBal := range params.SeedBalances { 72 affectedPublicKeys = append(affectedPublicKeys, &AffectedPublicKey{ 73 PublicKeyBase58Check: PkToString(seedBal.PublicKey, params), 74 Metadata: "GenesisBlockSeedBalance", 75 }) 76 totalOutput += seedBal.AmountNanos 77 } 78 err := DbPutTxindexTransactionMappings(txIndexDb, dummyTxn, params, &TransactionMetadata{ 79 TransactorPublicKeyBase58Check: dummyPk, 80 AffectedPublicKeys: affectedPublicKeys, 81 BlockHashHex: GenesisBlockHashHex, 82 TxnIndexInBlock: uint64(0), 83 // Just set some dummy metadata 84 BasicTransferTxindexMetadata: &BasicTransferTxindexMetadata{ 85 TotalInputNanos: 0, 86 TotalOutputNanos: totalOutput, 87 FeeNanos: 0, 88 }, 89 }) 90 if err != nil { 91 return nil, fmt.Errorf("NewTXIndex: Error initializing seed balances in txindex: %v", err) 92 } 93 } 94 95 // Add the other seed txns to the txn index. 96 for txnIndex, txnHex := range params.SeedTxns { 97 txnBytes, err := hex.DecodeString(txnHex) 98 if err != nil { 99 return nil, fmt.Errorf("NewTXIndex: Error decoding seed txn HEX: %v, txn index: %v, txn hex: %v", err, txnIndex, txnHex) 100 } 101 txn := &MsgDeSoTxn{} 102 if err := txn.FromBytes(txnBytes); err != nil { 103 return nil, fmt.Errorf("NewTXIndex: Error decoding seed txn BYTES: %v, txn index: %v, txn hex: %v", err, txnIndex, txnHex) 104 } 105 err = DbPutTxindexTransactionMappings(txIndexDb, txn, params, &TransactionMetadata{ 106 TransactorPublicKeyBase58Check: PkToString(txn.PublicKey, params), 107 // Note that we don't set AffectedPublicKeys for the SeedTxns 108 BlockHashHex: GenesisBlockHashHex, 109 TxnIndexInBlock: uint64(0), 110 // Just set some dummy metadata 111 BasicTransferTxindexMetadata: &BasicTransferTxindexMetadata{ 112 TotalInputNanos: 0, 113 TotalOutputNanos: 0, 114 FeeNanos: 0, 115 }, 116 }) 117 if err != nil { 118 return nil, fmt.Errorf("NewTXIndex: Error initializing seed txn %v in txindex: %v", txn, err) 119 } 120 } 121 } 122 123 // Ignore all the notifications from the txindex blockchain object 124 txIndexBlockchainNotificationChan := make(chan *ServerMessage, 1000) 125 go func() { 126 for { 127 <-txIndexBlockchainNotificationChan 128 } 129 }() 130 131 // Note that we *DONT* pass server here because it is already tied to the to the main blockchain. 132 txIndexChain, err := NewBlockchain( 133 []string{}, 0, 134 params, chainlib.NewMedianTime(), txIndexDb, nil, nil) 135 if err != nil { 136 return nil, fmt.Errorf("NewTXIndex: Error initializing TxIndex: %v", err) 137 } 138 139 // At this point, we should have set up a blockchain object for our 140 // txindex, and initialized all of the seed txns and seed balances 141 // correctly. Attaching blocks to our txnindex blockchain or adding 142 // txns to our txindex should work smoothly now. 143 144 return &TXIndex{ 145 TXIndexChain: txIndexChain, 146 CoreChain: coreChain, 147 Params: params, 148 stopUpdateChannel: make(chan struct{}), 149 }, nil 150 } 151 152 func (txi *TXIndex) Start() { 153 glog.Info("TXIndex: Starting update thread") 154 155 // Run a loop to continuously update the txindex. Note that this is a noop 156 // except when run the first time or when a new block has arrived. 157 go func() { 158 txi.updateWaitGroup.Add(1) 159 160 for { 161 select { 162 case <-txi.stopUpdateChannel: 163 txi.updateWaitGroup.Done() 164 return 165 default: 166 if txi.CoreChain.ChainState() == SyncStateFullyCurrent { 167 // If the node is fully synced, then try an update. 168 err := txi.Update() 169 if err != nil { 170 glog.Error(fmt.Errorf("tryUpdateTxindex: Problem running update: %v", err)) 171 } 172 } else { 173 glog.V(1).Infof("TXIndex: Waiting for node to sync before updating") 174 } 175 break 176 } 177 178 time.Sleep(1 * time.Second) 179 } 180 }() 181 } 182 183 func (txi *TXIndex) Stop() { 184 glog.Info("TXIndex: Stopping updates and closing database") 185 186 txi.stopUpdateChannel <- struct{}{} 187 txi.updateWaitGroup.Wait() 188 189 txi.TXIndexChain.DB().Close() 190 } 191 192 // GetTxindexUpdateBlockNodes ... 193 func (txi *TXIndex) GetTxindexUpdateBlockNodes() ( 194 _txindexTipNode *BlockNode, _blockTipNode *BlockNode, _commonAncestor *BlockNode, 195 _detachBlocks []*BlockNode, _attachBlocks []*BlockNode) { 196 197 // Get the current txindex tip. 198 txindexTipHash := txi.TXIndexChain.BlockTip() 199 if txindexTipHash == nil { 200 // The tip hash should never be nil since the txindex chain should have 201 // been initialized in the constructor. Print an error and return in this 202 // case. 203 glog.Error("Error: TXIndexChain had nil tip; this should never " + 204 "happen and it means the transaction index is broken.") 205 return nil, nil, nil, nil, nil 206 } 207 // If the tip of the txindex is no longer stored in the block index, it 208 // means the txindex hit a fork that we are no longer keeping track of. 209 // The only thing we can really do in this case is rebuild the entire index 210 // from scratch. To do that, we return all the blocks in the index to detach 211 // and all the blocks in the real chain to attach. 212 txindexTipNode := txi.CoreChain.CopyBlockIndex()[*txindexTipHash.Hash] 213 214 if txindexTipNode == nil { 215 glog.Info("GetTxindexUpdateBlockNodes: Txindex tip was not found; building txindex starting at genesis block") 216 217 newTxIndexBestChain, _ := txi.TXIndexChain.CopyBestChain() 218 newBlockchainBestChain, _ := txi.CoreChain.CopyBestChain() 219 220 return txindexTipNode, txi.CoreChain.BlockTip(), nil, newTxIndexBestChain, newBlockchainBestChain 221 } 222 223 // At this point, we know our txindex tip is in our block index so 224 // there must be a common ancestor between the tip and the block tip. 225 blockTip := txi.CoreChain.BlockTip() 226 commonAncestor, detachBlocks, attachBlocks := GetReorgBlocks(txindexTipNode, blockTip) 227 228 return txindexTipNode, blockTip, commonAncestor, detachBlocks, attachBlocks 229 } 230 231 // Update syncs the transaction index with the blockchain. 232 // Specifically, it reads in all the blocks that have come in since the last 233 // time this function was called and adds the new transactions to the txindex. 234 // It also handles reorgs properly. 235 // 236 // TODO(DELETEME, cleanup): This code is error-prone. Moving the transaction indexing code 237 // to block_view.go may be a clean way to refactor this. 238 func (txi *TXIndex) Update() error { 239 // If we don't have a chain set, return an error. 240 if txi.TXIndexChain == nil { 241 return fmt.Errorf("Update: Missing TXIndexChain") 242 } 243 244 // Lock the txindex and the blockchain for reading until we're 245 // done with the rest of the function. 246 txi.TXIndexLock.Lock() 247 defer txi.TXIndexLock.Unlock() 248 txindexTipNode, blockTipNode, commonAncestor, detachBlocks, attachBlocks := txi.GetTxindexUpdateBlockNodes() 249 250 // Note that the blockchain's ChainLock does not need to be held at this 251 // point because we're just reading blocks from the db, which never get 252 // deleted and therefore don't need the lock in order to access. 253 254 // If we get to this point, the commonAncestor should never be nil. 255 if commonAncestor == nil { 256 return fmt.Errorf("Update: Expected common ancestor "+ 257 "between txindex tip %v and block tip %v but found none; this "+ 258 "should never happen", txindexTipNode, blockTipNode) 259 } 260 // If the tip of the txindex is the same as the block tip, don't do 261 // an update. 262 if reflect.DeepEqual(txindexTipNode.Hash[:], blockTipNode.Hash[:]) { 263 glog.V(1).Infof("Update: Skipping update since block tip equals "+ 264 "txindex tip: Height: %d, Hash: %v", txindexTipNode.Height, txindexTipNode.Hash) 265 return nil 266 } 267 268 // When the txindex tip does not match the block tip then there's work 269 // to do. Log at the info level. 270 glog.Infof("Update: Updating txindex tip (height: %d, hash: %v) "+ 271 "to block tip (height: %d, hash: %v) ...", 272 txindexTipNode.Height, txindexTipNode.Hash, 273 blockTipNode.Height, blockTipNode.Hash) 274 275 // For each of the blocks we're removing, delete the transactions from 276 // the transaction index. 277 for _, blockToDetach := range detachBlocks { 278 // Go through each txn in the block and delete its mappings from our 279 // txindex. 280 glog.V(1).Infof("Update: Detaching block (height: %d, hash: %v)", 281 blockToDetach.Height, blockToDetach.Hash) 282 blockMsg, err := GetBlock(blockToDetach.Hash, txi.TXIndexChain.DB()) 283 if err != nil { 284 return fmt.Errorf("Update: Problem fetching detach block "+ 285 "with hash %v: %v", blockToDetach.Hash, err) 286 } 287 // Iterate through each transaction in the block and delete all its 288 // mappings from the db. Note the txindex has its own db that is 289 // distinct and isolated from our core blockchain db. 290 for _, txn := range blockMsg.Txns { 291 if err := DbDeleteTxindexTransactionMappings( 292 txi.TXIndexChain.DB(), txn, txi.Params); err != nil { 293 294 return fmt.Errorf("Update: Problem deleting "+ 295 "transaction mappings for transaction %v: %v", txn.Hash(), err) 296 } 297 } 298 299 // Now that all the transactions have been deleted from our txindex, 300 // it's safe to disconnect the block from our txindex chain. 301 utxoView, err := NewUtxoView(txi.TXIndexChain.DB(), txi.Params, nil) 302 if err != nil { 303 return fmt.Errorf( 304 "Update: Error initializing UtxoView: %v", err) 305 } 306 utxoOps, err := GetUtxoOperationsForBlock( 307 txi.TXIndexChain.DB(), blockToDetach.Hash) 308 if err != nil { 309 return fmt.Errorf( 310 "Update: Error getting UtxoOps for block %v: %v", blockToDetach, err) 311 } 312 // Compute the hashes for all the transactions. 313 txHashes, err := ComputeTransactionHashes(blockMsg.Txns) 314 if err != nil { 315 return fmt.Errorf( 316 "Update: Error computing tx hashes for block %v: %v", 317 blockToDetach, err) 318 } 319 if err := utxoView.DisconnectBlock(blockMsg, txHashes, utxoOps); err != nil { 320 return fmt.Errorf("Update: Error detaching block "+ 321 "%v from UtxoView: %v", blockToDetach, err) 322 } 323 if err := utxoView.FlushToDb(); err != nil { 324 return fmt.Errorf("Update: Error flushing view to db for block "+ 325 "%v: %v", blockToDetach, err) 326 } 327 // We have to flush a couple of extra things that the view doesn't flush... 328 if err := PutBestHash(utxoView.TipHash, txi.TXIndexChain.DB(), ChainTypeDeSoBlock); err != nil { 329 return fmt.Errorf("Update: Error putting best hash for block "+ 330 "%v: %v", blockToDetach, err) 331 } 332 err = txi.TXIndexChain.DB().Update(func(txn *badger.Txn) error { 333 if err := DeleteUtxoOperationsForBlockWithTxn(txn, blockToDetach.Hash); err != nil { 334 return fmt.Errorf("Update: Error deleting UtxoOperations 1 for block %v, %v", blockToDetach.Hash, err) 335 } 336 if err := txn.Delete(BlockHashToBlockKey(blockToDetach.Hash)); err != nil { 337 return fmt.Errorf("Update: Error deleting UtxoOperations 2 for block %v %v", blockToDetach.Hash, err) 338 } 339 return nil 340 }) 341 if err != nil { 342 return fmt.Errorf("Update: Error updating badgger: %v", err) 343 } 344 // Delete this block from the chain db so we don't get duplicate block errors. 345 346 // Remove this block from our bestChain data structures. 347 newBlockIndex := txi.TXIndexChain.CopyBlockIndex() 348 newBestChain, newBestChainMap := txi.TXIndexChain.CopyBestChain() 349 newBestChain = newBestChain[:len(newBestChain)-1] 350 delete(newBestChainMap, *(blockToDetach.Hash)) 351 delete(newBlockIndex, *(blockToDetach.Hash)) 352 353 txi.TXIndexChain.SetBestChainMap(newBestChain, newBestChainMap, newBlockIndex) 354 355 // At this point the entries for the block should have been removed 356 // from both our Txindex chain and our transaction index mappings. 357 } 358 359 // For each of the blocks we're adding, process them on our txindex chain 360 // and add their mappings to our txn index. Compute any metadata that might 361 // be useful. 362 for _, blockToAttach := range attachBlocks { 363 if blockToAttach.Height%100 == 0 { 364 glog.Infof("Update: Txindex progress: block %d / %d", 365 blockToAttach.Height, blockTipNode.Height) 366 } 367 glog.V(2).Infof("Update: Attaching block (height: %d, hash: %v)", 368 blockToAttach.Height, blockToAttach.Hash) 369 370 blockMsg, err := GetBlock(blockToAttach.Hash, txi.CoreChain.DB()) 371 if err != nil { 372 return fmt.Errorf("Update: Problem fetching attach block "+ 373 "with hash %v: %v", blockToAttach.Hash, err) 374 } 375 376 // We use a view to simulate adding transactions to our chain. This allows 377 // us to extract custom metadata fields that we can show in our block explorer. 378 // 379 // Only set a BitcoinManager if we have one. This makes some tests pass. 380 utxoView, err := NewUtxoView(txi.TXIndexChain.DB(), txi.Params, nil) 381 if err != nil { 382 return fmt.Errorf( 383 "Update: Error initializing UtxoView: %v", err) 384 } 385 386 // Do each block update in a single transaction so we're safe in case the node 387 // restarts. 388 txi.TXIndexChain.DB().Update(func(dbTxn *badger.Txn) error { 389 390 // Iterate through each transaction in the block and do the following: 391 // - Connect it to the view 392 // - Compute its mapping values, which may include custom metadata fields 393 // - add all its mappings to the db. 394 for txnIndexInBlock, txn := range blockMsg.Txns { 395 txnMeta, err := ConnectTxnAndComputeTransactionMetadata( 396 txn, utxoView, blockToAttach.Hash, blockToAttach.Height, uint64(txnIndexInBlock)) 397 if err != nil { 398 return fmt.Errorf("Update: Problem connecting txn %v to txindex: %v", 399 txn, err) 400 } 401 402 err = DbPutTxindexTransactionMappingsWithTxn(dbTxn, txn, txi.Params, txnMeta) 403 if err != nil { 404 return fmt.Errorf("Update: Problem adding txn %v to txindex: %v", 405 txn, err) 406 } 407 } 408 409 return nil 410 }) 411 412 // Now that we have added all the txns to our TxIndex db, attach the block 413 // to update our chain. 414 _, _, err = txi.TXIndexChain.ProcessBlock(blockMsg, false /*verifySignatures*/) 415 if err != nil { 416 return fmt.Errorf("Update: Problem attaching block %v: %v", 417 blockToAttach, err) 418 } 419 } 420 421 glog.Infof("Update: Txindex update complete. New tip: (height: %d, hash: %v)", 422 txi.TXIndexChain.BlockTip().Height, txi.TXIndexChain.BlockTip().Hash) 423 424 return nil 425 }