github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/modules/wallet/update.go (about) 1 package wallet 2 3 import ( 4 "math" 5 6 "SiaPrime/modules" 7 "SiaPrime/types" 8 "gitlab.com/NebulousLabs/errors" 9 10 "gitlab.com/NebulousLabs/bolt" 11 ) 12 13 type ( 14 spentSiacoinOutputSet map[types.SiacoinOutputID]types.SiacoinOutput 15 spentSiafundOutputSet map[types.SiafundOutputID]types.SiafundOutput 16 ) 17 18 // threadedResetSubscriptions unsubscribes the wallet from the consensus set and transaction pool 19 // and subscribes again. 20 func (w *Wallet) threadedResetSubscriptions() error { 21 if !w.scanLock.TryLock() { 22 return errScanInProgress 23 } 24 defer w.scanLock.Unlock() 25 26 w.cs.Unsubscribe(w) 27 w.tpool.Unsubscribe(w) 28 29 err := w.cs.ConsensusSetSubscribe(w, modules.ConsensusChangeBeginning, w.tg.StopChan()) 30 if err != nil { 31 return err 32 } 33 w.tpool.TransactionPoolSubscribe(w) 34 return nil 35 } 36 37 // advanceSeedLookahead generates all keys from the current primary seed progress up to index 38 // and adds them to the set of spendable keys. Therefore the new primary seed progress will 39 // be index+1 and new lookahead keys will be generated starting from index+1 40 // Returns true if a blockchain rescan is required 41 func (w *Wallet) advanceSeedLookahead(index uint64) (bool, error) { 42 progress, err := dbGetPrimarySeedProgress(w.dbTx) 43 if err != nil { 44 return false, err 45 } 46 newProgress := index + 1 47 48 // Add spendable keys and remove them from lookahead 49 spendableKeys := generateKeys(w.primarySeed, progress, newProgress-progress) 50 for _, key := range spendableKeys { 51 w.keys[key.UnlockConditions.UnlockHash()] = key 52 delete(w.lookahead, key.UnlockConditions.UnlockHash()) 53 } 54 55 // Update the primarySeedProgress 56 dbPutPrimarySeedProgress(w.dbTx, newProgress) 57 if err != nil { 58 return false, err 59 } 60 61 // Regenerate lookahead 62 w.regenerateLookahead(newProgress) 63 64 // If more than lookaheadRescanThreshold keys were generated 65 // also initialize a rescan just to be safe. 66 if uint64(len(spendableKeys)) > lookaheadRescanThreshold { 67 return true, nil 68 } 69 70 return false, nil 71 } 72 73 // isWalletAddress is a helper function that checks if an UnlockHash is 74 // derived from one of the wallet's spendable keys or is being explicitly watched. 75 func (w *Wallet) isWalletAddress(uh types.UnlockHash) bool { 76 _, spendable := w.keys[uh] 77 _, watchonly := w.watchedAddrs[uh] 78 return spendable || watchonly 79 } 80 81 // updateLookahead uses a consensus change to update the seed progress if one of the outputs 82 // contains an unlock hash of the lookahead set. Returns true if a blockchain rescan is required 83 func (w *Wallet) updateLookahead(tx *bolt.Tx, cc modules.ConsensusChange) (bool, error) { 84 var largestIndex uint64 85 for _, diff := range cc.SiacoinOutputDiffs { 86 if index, ok := w.lookahead[diff.SiacoinOutput.UnlockHash]; ok { 87 if index > largestIndex { 88 largestIndex = index 89 } 90 } 91 } 92 for _, diff := range cc.SiafundOutputDiffs { 93 if index, ok := w.lookahead[diff.SiafundOutput.UnlockHash]; ok { 94 if index > largestIndex { 95 largestIndex = index 96 } 97 } 98 } 99 if largestIndex > 0 { 100 return w.advanceSeedLookahead(largestIndex) 101 } 102 103 return false, nil 104 } 105 106 // updateConfirmedSet uses a consensus change to update the confirmed set of 107 // outputs as understood by the wallet. 108 func (w *Wallet) updateConfirmedSet(tx *bolt.Tx, cc modules.ConsensusChange) error { 109 for _, diff := range cc.SiacoinOutputDiffs { 110 // Verify that the diff is relevant to the wallet. 111 if !w.isWalletAddress(diff.SiacoinOutput.UnlockHash) { 112 continue 113 } 114 115 var err error 116 if diff.Direction == modules.DiffApply { 117 w.log.Println("Wallet has gained a spendable siacoin output:", diff.ID, "::", diff.SiacoinOutput.Value.HumanString()) 118 err = dbPutSiacoinOutput(tx, diff.ID, diff.SiacoinOutput) 119 } else { 120 w.log.Println("Wallet has lost a spendable siacoin output:", diff.ID, "::", diff.SiacoinOutput.Value.HumanString()) 121 err = dbDeleteSiacoinOutput(tx, diff.ID) 122 } 123 if err != nil { 124 w.log.Severe("Could not update siacoin output:", err) 125 return err 126 } 127 } 128 for _, diff := range cc.SiafundOutputDiffs { 129 // Verify that the diff is relevant to the wallet. 130 if !w.isWalletAddress(diff.SiafundOutput.UnlockHash) { 131 continue 132 } 133 134 var err error 135 if diff.Direction == modules.DiffApply { 136 w.log.Println("Wallet has gained a spendable siafund output:", diff.ID, "::", diff.SiafundOutput.Value) 137 err = dbPutSiafundOutput(tx, diff.ID, diff.SiafundOutput) 138 } else { 139 w.log.Println("Wallet has lost a spendable siafund output:", diff.ID, "::", diff.SiafundOutput.Value) 140 err = dbDeleteSiafundOutput(tx, diff.ID) 141 } 142 if err != nil { 143 w.log.Severe("Could not update siafund output:", err) 144 return err 145 } 146 } 147 for _, diff := range cc.SiafundPoolDiffs { 148 var err error 149 if diff.Direction == modules.DiffApply { 150 err = dbPutSiafundPool(tx, diff.Adjusted) 151 } else { 152 err = dbPutSiafundPool(tx, diff.Previous) 153 } 154 if err != nil { 155 w.log.Severe("Could not update siafund pool:", err) 156 return err 157 } 158 } 159 return nil 160 } 161 162 // revertHistory reverts any transaction history that was destroyed by reverted 163 // blocks in the consensus change. 164 func (w *Wallet) revertHistory(tx *bolt.Tx, reverted []types.Block) error { 165 for _, block := range reverted { 166 // Remove any transactions that have been reverted. 167 for i := len(block.Transactions) - 1; i >= 0; i-- { 168 // If the transaction is relevant to the wallet, it will be the 169 // most recent transaction in bucketProcessedTransactions. 170 txid := block.Transactions[i].ID() 171 pt, err := dbGetLastProcessedTransaction(tx) 172 if err != nil { 173 break // bucket is empty 174 } 175 if txid == pt.TransactionID { 176 w.log.Println("A wallet transaction has been reverted due to a reorg:", txid) 177 if err := dbDeleteLastProcessedTransaction(tx); err != nil { 178 w.log.Severe("Could not revert transaction:", err) 179 return err 180 } 181 } 182 } 183 184 // Remove the miner payout transaction if applicable. 185 for i, mp := range block.MinerPayouts { 186 // If the transaction is relevant to the wallet, it will be the 187 // most recent transaction in bucketProcessedTransactions. 188 pt, err := dbGetLastProcessedTransaction(tx) 189 if err != nil { 190 break // bucket is empty 191 } 192 if types.TransactionID(block.ID()) == pt.TransactionID { 193 w.log.Println("Miner payout has been reverted due to a reorg:", block.MinerPayoutID(uint64(i)), "::", mp.Value.HumanString()) 194 if err := dbDeleteLastProcessedTransaction(tx); err != nil { 195 w.log.Severe("Could not revert transaction:", err) 196 return err 197 } 198 break // there will only ever be one miner transaction 199 } 200 } 201 202 // decrement the consensus height 203 if block.ID() != types.GenesisID { 204 consensusHeight, err := dbGetConsensusHeight(tx) 205 if err != nil { 206 return err 207 } 208 err = dbPutConsensusHeight(tx, consensusHeight-1) 209 if err != nil { 210 return err 211 } 212 } 213 } 214 return nil 215 } 216 217 // outputs and collects them in a map of SiacoinOutputID -> SiacoinOutput. 218 func computeSpentSiacoinOutputSet(diffs []modules.SiacoinOutputDiff) spentSiacoinOutputSet { 219 outputs := make(spentSiacoinOutputSet) 220 for _, diff := range diffs { 221 if diff.Direction == modules.DiffRevert { 222 // DiffRevert means spent. 223 outputs[diff.ID] = diff.SiacoinOutput 224 } 225 } 226 return outputs 227 } 228 229 // computeSpentSiafundOutputSet scans a slice of Siafund output diffs for spent 230 // outputs and collects them in a map of SiafundOutputID -> SiafundOutput. 231 func computeSpentSiafundOutputSet(diffs []modules.SiafundOutputDiff) spentSiafundOutputSet { 232 outputs := make(spentSiafundOutputSet) 233 for _, diff := range diffs { 234 if diff.Direction == modules.DiffRevert { 235 // DiffRevert means spent. 236 outputs[diff.ID] = diff.SiafundOutput 237 } 238 } 239 return outputs 240 } 241 242 // computeProcessedTransactionsFromBlock searches all the miner payouts and 243 // transactions in a block and computes a ProcessedTransaction slice containing 244 // all of the transactions processed for the given block. 245 func (w *Wallet) computeProcessedTransactionsFromBlock(tx *bolt.Tx, block types.Block, spentSiacoinOutputs spentSiacoinOutputSet, spentSiafundOutputs spentSiafundOutputSet, consensusHeight types.BlockHeight) []modules.ProcessedTransaction { 246 var pts []modules.ProcessedTransaction 247 248 // Find ProcessedTransactions from miner payouts. 249 relevant := false 250 for _, mp := range block.MinerPayouts { 251 relevant = relevant || w.isWalletAddress(mp.UnlockHash) 252 } 253 if relevant { 254 w.log.Println("Wallet has received new miner payouts:", block.ID()) 255 // Apply the miner payout transaction if applicable. 256 minerPT := modules.ProcessedTransaction{ 257 Transaction: types.Transaction{}, 258 TransactionID: types.TransactionID(block.ID()), 259 ConfirmationHeight: consensusHeight, 260 ConfirmationTimestamp: block.Timestamp, 261 } 262 for i, mp := range block.MinerPayouts { 263 w.log.Println("\tminer payout:", block.MinerPayoutID(uint64(i)), "::", mp.Value.HumanString()) 264 minerPT.Outputs = append(minerPT.Outputs, modules.ProcessedOutput{ 265 ID: types.OutputID(block.MinerPayoutID(uint64(i))), 266 FundType: types.SpecifierMinerPayout, 267 MaturityHeight: consensusHeight + types.MaturityDelay, 268 WalletAddress: w.isWalletAddress(mp.UnlockHash), 269 RelatedAddress: mp.UnlockHash, 270 Value: mp.Value, 271 }) 272 } 273 pts = append(pts, minerPT) 274 } 275 276 // Find ProcessedTransactions from transactions. 277 for _, txn := range block.Transactions { 278 // Determine if transaction is relevant. 279 relevant := false 280 for _, sci := range txn.SiacoinInputs { 281 relevant = relevant || w.isWalletAddress(sci.UnlockConditions.UnlockHash()) 282 } 283 for _, sco := range txn.SiacoinOutputs { 284 relevant = relevant || w.isWalletAddress(sco.UnlockHash) 285 } 286 for _, sfi := range txn.SiafundInputs { 287 relevant = relevant || w.isWalletAddress(sfi.UnlockConditions.UnlockHash()) 288 } 289 for _, sfo := range txn.SiafundOutputs { 290 relevant = relevant || w.isWalletAddress(sfo.UnlockHash) 291 } 292 293 // Only create a ProcessedTransaction if transaction is relevant. 294 if !relevant { 295 continue 296 } 297 w.log.Println("A transaction has been confirmed on the blockchain:", txn.ID()) 298 299 pt := modules.ProcessedTransaction{ 300 Transaction: txn, 301 TransactionID: txn.ID(), 302 ConfirmationHeight: consensusHeight, 303 ConfirmationTimestamp: block.Timestamp, 304 } 305 306 for _, sci := range txn.SiacoinInputs { 307 pi := modules.ProcessedInput{ 308 ParentID: types.OutputID(sci.ParentID), 309 FundType: types.SpecifierSiacoinInput, 310 WalletAddress: w.isWalletAddress(sci.UnlockConditions.UnlockHash()), 311 RelatedAddress: sci.UnlockConditions.UnlockHash(), 312 Value: spentSiacoinOutputs[sci.ParentID].Value, 313 } 314 pt.Inputs = append(pt.Inputs, pi) 315 316 // Log any wallet-relevant inputs. 317 if pi.WalletAddress { 318 w.log.Println("\tSiacoin Input:", pi.ParentID, "::", pi.Value.HumanString()) 319 } 320 } 321 322 for i, sco := range txn.SiacoinOutputs { 323 po := modules.ProcessedOutput{ 324 ID: types.OutputID(txn.SiacoinOutputID(uint64(i))), 325 FundType: types.SpecifierSiacoinOutput, 326 MaturityHeight: consensusHeight, 327 WalletAddress: w.isWalletAddress(sco.UnlockHash), 328 RelatedAddress: sco.UnlockHash, 329 Value: sco.Value, 330 } 331 pt.Outputs = append(pt.Outputs, po) 332 333 // Log any wallet-relevant outputs. 334 if po.WalletAddress { 335 w.log.Println("\tSiacoin Output:", po.ID, "::", po.Value.HumanString()) 336 } 337 } 338 339 for _, sfi := range txn.SiafundInputs { 340 pi := modules.ProcessedInput{ 341 ParentID: types.OutputID(sfi.ParentID), 342 FundType: types.SpecifierSiafundInput, 343 WalletAddress: w.isWalletAddress(sfi.UnlockConditions.UnlockHash()), 344 RelatedAddress: sfi.UnlockConditions.UnlockHash(), 345 Value: spentSiafundOutputs[sfi.ParentID].Value, 346 } 347 pt.Inputs = append(pt.Inputs, pi) 348 // Log any wallet-relevant inputs. 349 if pi.WalletAddress { 350 w.log.Println("\tSiafund Input:", pi.ParentID, "::", pi.Value.HumanString()) 351 } 352 353 siafundPool, err := dbGetSiafundPool(w.dbTx) 354 if err != nil { 355 w.log.Println("could not get siafund pool: ", err) 356 continue 357 } 358 359 sfo := spentSiafundOutputs[sfi.ParentID] 360 po := modules.ProcessedOutput{ 361 ID: types.OutputID(sfi.ParentID), 362 FundType: types.SpecifierClaimOutput, 363 MaturityHeight: consensusHeight + types.MaturityDelay, 364 WalletAddress: w.isWalletAddress(sfi.UnlockConditions.UnlockHash()), 365 RelatedAddress: sfi.ClaimUnlockHash, 366 Value: siafundPool.Sub(sfo.ClaimStart).Mul(sfo.Value), 367 } 368 pt.Outputs = append(pt.Outputs, po) 369 // Log any wallet-relevant outputs. 370 if po.WalletAddress { 371 w.log.Println("\tClaim Output:", po.ID, "::", po.Value.HumanString()) 372 } 373 } 374 375 for i, sfo := range txn.SiafundOutputs { 376 po := modules.ProcessedOutput{ 377 ID: types.OutputID(txn.SiafundOutputID(uint64(i))), 378 FundType: types.SpecifierSiafundOutput, 379 MaturityHeight: consensusHeight, 380 WalletAddress: w.isWalletAddress(sfo.UnlockHash), 381 RelatedAddress: sfo.UnlockHash, 382 Value: sfo.Value, 383 } 384 pt.Outputs = append(pt.Outputs, po) 385 // Log any wallet-relevant outputs. 386 if po.WalletAddress { 387 w.log.Println("\tSiafund Output:", po.ID, "::", po.Value.HumanString()) 388 } 389 } 390 391 for _, fee := range txn.MinerFees { 392 pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{ 393 FundType: types.SpecifierMinerFee, 394 MaturityHeight: consensusHeight + types.MaturityDelay, 395 Value: fee, 396 }) 397 } 398 pts = append(pts, pt) 399 } 400 return pts 401 } 402 403 // applyHistory applies any transaction history that the applied blocks 404 // introduced. 405 func (w *Wallet) applyHistory(tx *bolt.Tx, cc modules.ConsensusChange) error { 406 spentSiacoinOutputs := computeSpentSiacoinOutputSet(cc.SiacoinOutputDiffs) 407 spentSiafundOutputs := computeSpentSiafundOutputSet(cc.SiafundOutputDiffs) 408 409 for _, block := range cc.AppliedBlocks { 410 consensusHeight, err := dbGetConsensusHeight(tx) 411 if err != nil { 412 return errors.AddContext(err, "failed to consensus height") 413 } 414 // Increment the consensus height. 415 if block.ID() != types.GenesisID { 416 consensusHeight++ 417 err = dbPutConsensusHeight(tx, consensusHeight) 418 if err != nil { 419 return errors.AddContext(err, "failed to store consensus height in database") 420 } 421 } 422 423 pts := w.computeProcessedTransactionsFromBlock(tx, block, spentSiacoinOutputs, spentSiafundOutputs, consensusHeight) 424 for _, pt := range pts { 425 err := dbAppendProcessedTransaction(tx, pt) 426 if err != nil { 427 return errors.AddContext(err, "could not put processed transaction") 428 } 429 } 430 } 431 432 return nil 433 } 434 435 // ProcessConsensusChange parses a consensus change to update the set of 436 // confirmed outputs known to the wallet. 437 func (w *Wallet) ProcessConsensusChange(cc modules.ConsensusChange) { 438 if err := w.tg.Add(); err != nil { 439 return 440 } 441 defer w.tg.Done() 442 443 w.mu.Lock() 444 defer w.mu.Unlock() 445 446 if needRescan, err := w.updateLookahead(w.dbTx, cc); err != nil { 447 w.log.Severe("ERROR: failed to update lookahead:", err) 448 w.dbRollback = true 449 } else if needRescan { 450 go w.threadedResetSubscriptions() 451 } 452 if err := w.updateConfirmedSet(w.dbTx, cc); err != nil { 453 w.log.Severe("ERROR: failed to update confirmed set:", err) 454 w.dbRollback = true 455 } 456 if err := w.revertHistory(w.dbTx, cc.RevertedBlocks); err != nil { 457 w.log.Severe("ERROR: failed to revert consensus change:", err) 458 w.dbRollback = true 459 } 460 if err := w.applyHistory(w.dbTx, cc); err != nil { 461 w.log.Severe("ERROR: failed to apply consensus change:", err) 462 w.dbRollback = true 463 } 464 if err := dbPutConsensusChangeID(w.dbTx, cc.ID); err != nil { 465 w.log.Severe("ERROR: failed to update consensus change ID:", err) 466 w.dbRollback = true 467 } 468 469 if cc.Synced { 470 go w.threadedDefragWallet() 471 } 472 } 473 474 // ReceiveUpdatedUnconfirmedTransactions updates the wallet's unconfirmed 475 // transaction set. 476 func (w *Wallet) ReceiveUpdatedUnconfirmedTransactions(diff *modules.TransactionPoolDiff) { 477 if err := w.tg.Add(); err != nil { 478 return 479 } 480 defer w.tg.Done() 481 482 w.mu.Lock() 483 defer w.mu.Unlock() 484 485 // Do the pruning first. If there are any pruned transactions, we will need 486 // to re-allocate the whole processed transactions array. 487 droppedTransactions := make(map[types.TransactionID]struct{}) 488 for i := range diff.RevertedTransactions { 489 txids := w.unconfirmedSets[diff.RevertedTransactions[i]] 490 for i := range txids { 491 droppedTransactions[txids[i]] = struct{}{} 492 } 493 delete(w.unconfirmedSets, diff.RevertedTransactions[i]) 494 } 495 496 // Skip the reallocation if we can, otherwise reallocate the 497 // unconfirmedProcessedTransactions to no longer have the dropped 498 // transactions. 499 if len(droppedTransactions) != 0 { 500 // Capacity can't be reduced, because we have no way of knowing if the 501 // dropped transactions are relevant to the wallet or not, and some will 502 // not be relevant to the wallet, meaning they don't have a counterpart 503 // in w.unconfirmedProcessedTransactions. 504 newUPT := make([]modules.ProcessedTransaction, 0, len(w.unconfirmedProcessedTransactions)) 505 for _, txn := range w.unconfirmedProcessedTransactions { 506 _, exists := droppedTransactions[txn.TransactionID] 507 if !exists { 508 // Transaction was not dropped, add it to the new unconfirmed 509 // transactions. 510 newUPT = append(newUPT, txn) 511 } 512 } 513 514 // Set the unconfirmed preocessed transactions to the pruned set. 515 w.unconfirmedProcessedTransactions = newUPT 516 } 517 518 // Scroll through all of the diffs and add any new transactions. 519 for _, unconfirmedTxnSet := range diff.AppliedTransactions { 520 // Mark all of the transactions that appeared in this set. 521 // 522 // TODO: Technically only necessary to mark the ones that are relevant 523 // to the wallet, but overhead should be low. 524 w.unconfirmedSets[unconfirmedTxnSet.ID] = unconfirmedTxnSet.IDs 525 526 // Get the values for the spent outputs. 527 spentSiacoinOutputs := make(map[types.SiacoinOutputID]types.SiacoinOutput) 528 for _, scod := range unconfirmedTxnSet.Change.SiacoinOutputDiffs { 529 // Only need to grab the reverted ones, because only reverted ones 530 // have the possibility of having been spent. 531 if scod.Direction == modules.DiffRevert { 532 spentSiacoinOutputs[scod.ID] = scod.SiacoinOutput 533 } 534 } 535 536 // Add each transaction to our set of unconfirmed transactions. 537 for i, txn := range unconfirmedTxnSet.Transactions { 538 // determine whether transaction is relevant to the wallet 539 relevant := false 540 for _, sci := range txn.SiacoinInputs { 541 relevant = relevant || w.isWalletAddress(sci.UnlockConditions.UnlockHash()) 542 } 543 for _, sco := range txn.SiacoinOutputs { 544 relevant = relevant || w.isWalletAddress(sco.UnlockHash) 545 } 546 547 // only create a ProcessedTransaction if txn is relevant 548 if !relevant { 549 continue 550 } 551 552 pt := modules.ProcessedTransaction{ 553 Transaction: txn, 554 TransactionID: unconfirmedTxnSet.IDs[i], 555 ConfirmationHeight: types.BlockHeight(math.MaxUint64), 556 ConfirmationTimestamp: types.Timestamp(math.MaxUint64), 557 } 558 for _, sci := range txn.SiacoinInputs { 559 pt.Inputs = append(pt.Inputs, modules.ProcessedInput{ 560 ParentID: types.OutputID(sci.ParentID), 561 FundType: types.SpecifierSiacoinInput, 562 WalletAddress: w.isWalletAddress(sci.UnlockConditions.UnlockHash()), 563 RelatedAddress: sci.UnlockConditions.UnlockHash(), 564 Value: spentSiacoinOutputs[sci.ParentID].Value, 565 }) 566 } 567 for i, sco := range txn.SiacoinOutputs { 568 pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{ 569 ID: types.OutputID(txn.SiacoinOutputID(uint64(i))), 570 FundType: types.SpecifierSiacoinOutput, 571 MaturityHeight: types.BlockHeight(math.MaxUint64), 572 WalletAddress: w.isWalletAddress(sco.UnlockHash), 573 RelatedAddress: sco.UnlockHash, 574 Value: sco.Value, 575 }) 576 } 577 for _, fee := range txn.MinerFees { 578 pt.Outputs = append(pt.Outputs, modules.ProcessedOutput{ 579 FundType: types.SpecifierMinerFee, 580 Value: fee, 581 }) 582 } 583 w.unconfirmedProcessedTransactions = append(w.unconfirmedProcessedTransactions, pt) 584 } 585 } 586 }