github.com/nebulouslabs/sia@v1.3.7/modules/wallet/defrag.go (about) 1 package wallet 2 3 import ( 4 "errors" 5 "sort" 6 7 "github.com/NebulousLabs/Sia/crypto" 8 "github.com/NebulousLabs/Sia/types" 9 ) 10 11 var ( 12 errDefragNotNeeded = errors.New("defragging not needed, wallet is already sufficiently defragged") 13 ) 14 15 // managedCreateDefragTransaction creates a transaction that spends multiple existing 16 // wallet outputs into a single new address. 17 func (w *Wallet) managedCreateDefragTransaction() ([]types.Transaction, error) { 18 // dustThreshold and minFee have to be obtained separate from the lock 19 dustThreshold, err := w.DustThreshold() 20 if err != nil { 21 return nil, err 22 } 23 minFee, _ := w.tpool.FeeEstimation() 24 25 w.mu.Lock() 26 defer w.mu.Unlock() 27 28 consensusHeight, err := dbGetConsensusHeight(w.dbTx) 29 if err != nil { 30 return nil, err 31 } 32 33 // Collect a value-sorted set of siacoin outputs. 34 var so sortedOutputs 35 err = dbForEachSiacoinOutput(w.dbTx, func(scoid types.SiacoinOutputID, sco types.SiacoinOutput) { 36 if w.checkOutput(w.dbTx, consensusHeight, scoid, sco, dustThreshold) == nil { 37 so.ids = append(so.ids, scoid) 38 so.outputs = append(so.outputs, sco) 39 } 40 }) 41 if err != nil { 42 return nil, err 43 } 44 sort.Sort(sort.Reverse(so)) 45 46 // Only defrag if there are enough outputs to merit defragging. 47 if len(so.ids) <= defragThreshold { 48 return nil, errDefragNotNeeded 49 } 50 51 // Skip over the 'defragStartIndex' largest outputs, so that the user can 52 // still reasonably use their wallet while the defrag is happening. 53 var amount types.Currency 54 var parentTxn types.Transaction 55 var spentScoids []types.SiacoinOutputID 56 for i := defragStartIndex; i < defragStartIndex+defragBatchSize; i++ { 57 scoid := so.ids[i] 58 sco := so.outputs[i] 59 60 // Add a siacoin input for this output. 61 outputUnlockConditions := w.keys[sco.UnlockHash].UnlockConditions 62 sci := types.SiacoinInput{ 63 ParentID: scoid, 64 UnlockConditions: outputUnlockConditions, 65 } 66 parentTxn.SiacoinInputs = append(parentTxn.SiacoinInputs, sci) 67 spentScoids = append(spentScoids, scoid) 68 69 // Add the output to the total fund 70 amount = amount.Add(sco.Value) 71 } 72 73 // Create and add the output that will be used to fund the defrag 74 // transaction. 75 parentUnlockConditions, err := w.nextPrimarySeedAddress(w.dbTx) 76 if err != nil { 77 return nil, err 78 } 79 exactOutput := types.SiacoinOutput{ 80 Value: amount, 81 UnlockHash: parentUnlockConditions.UnlockHash(), 82 } 83 parentTxn.SiacoinOutputs = append(parentTxn.SiacoinOutputs, exactOutput) 84 85 // Sign all of the inputs to the parent transaction. 86 for _, sci := range parentTxn.SiacoinInputs { 87 addSignatures(&parentTxn, types.FullCoveredFields, sci.UnlockConditions, crypto.Hash(sci.ParentID), w.keys[sci.UnlockConditions.UnlockHash()]) 88 } 89 90 // Create the defrag transaction. 91 refundAddr, err := w.nextPrimarySeedAddress(w.dbTx) 92 if err != nil { 93 return nil, err 94 } 95 96 // compute the transaction fee. 97 sizeAvgOutput := uint64(250) 98 fee := minFee.Mul64(sizeAvgOutput * defragBatchSize) 99 100 txn := types.Transaction{ 101 SiacoinInputs: []types.SiacoinInput{{ 102 ParentID: parentTxn.SiacoinOutputID(0), 103 UnlockConditions: parentUnlockConditions, 104 }}, 105 SiacoinOutputs: []types.SiacoinOutput{{ 106 Value: amount.Sub(fee), 107 UnlockHash: refundAddr.UnlockHash(), 108 }}, 109 MinerFees: []types.Currency{fee}, 110 } 111 addSignatures(&txn, types.FullCoveredFields, parentUnlockConditions, crypto.Hash(parentTxn.SiacoinOutputID(0)), w.keys[parentUnlockConditions.UnlockHash()]) 112 113 // Mark all outputs that were spent as spent. 114 for _, scoid := range spentScoids { 115 if err = dbPutSpentOutput(w.dbTx, types.OutputID(scoid), consensusHeight); err != nil { 116 return nil, err 117 } 118 } 119 // Mark the parent output as spent. Must be done after the transaction is 120 // finished because otherwise the txid and output id will change. 121 if err = dbPutSpentOutput(w.dbTx, types.OutputID(parentTxn.SiacoinOutputID(0)), consensusHeight); err != nil { 122 return nil, err 123 } 124 125 // Construct the final transaction set 126 return []types.Transaction{parentTxn, txn}, nil 127 } 128 129 // threadedDefragWallet computes the sum of the 15 largest outputs in the wallet and 130 // sends that sum to itself, effectively defragmenting the wallet. This defrag 131 // operation is only performed if the wallet has greater than defragThreshold 132 // outputs. 133 func (w *Wallet) threadedDefragWallet() { 134 // Don't defrag if it was disabled 135 if w.defragDisabled { 136 return 137 } 138 139 err := w.tg.Add() 140 if err != nil { 141 return 142 } 143 defer w.tg.Done() 144 145 // Check that a defrag makes sense. 146 w.mu.RLock() 147 unlocked := w.unlocked 148 w.mu.RUnlock() 149 if !unlocked { 150 // Can't defrag if the wallet is locked. 151 return 152 } 153 154 // Create the defrag transaction. 155 txnSet, err := w.managedCreateDefragTransaction() 156 defer func() { 157 if err == nil { 158 return 159 } 160 w.mu.Lock() 161 defer w.mu.Unlock() 162 for _, txn := range txnSet { 163 for _, sci := range txn.SiacoinInputs { 164 dbDeleteSpentOutput(w.dbTx, types.OutputID(sci.ParentID)) 165 } 166 } 167 }() 168 if err == errDefragNotNeeded { 169 // begin 170 return 171 } else if err != nil { 172 w.log.Println("WARN: couldn't create defrag transaction:", err) 173 return 174 } 175 176 if w.deps.Disrupt("DefragInterrupted") { 177 err = errors.New("defrag was interrupted (DefragInterrupted)") 178 return 179 } 180 // Submit the defrag to the transaction pool. 181 err = w.tpool.AcceptTransactionSet(txnSet) 182 if err != nil { 183 w.log.Println("WARN: defrag transaction was rejected:", err) 184 return 185 } 186 w.log.Println("Submitting a transaction set to defragment the wallet's outputs, IDs:") 187 for _, txn := range txnSet { 188 w.log.Println("Wallet defrag: \t", txn.ID()) 189 } 190 }