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