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