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