gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/wallet/defrag.go (about) 1 package wallet 2 3 import ( 4 "errors" 5 "sort" 6 7 "gitlab.com/SiaPrime/SiaPrime/crypto" 8 "gitlab.com/SiaPrime/SiaPrime/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()], consensusHeight) 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()], consensusHeight) 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 w.mu.RLock() 136 disabled := w.defragDisabled 137 w.mu.RUnlock() 138 if disabled { 139 return 140 } 141 142 err := w.tg.Add() 143 if err != nil { 144 return 145 } 146 defer w.tg.Done() 147 148 // Check that a defrag makes sense. 149 w.mu.RLock() 150 unlocked := w.unlocked 151 w.mu.RUnlock() 152 if !unlocked { 153 // Can't defrag if the wallet is locked. 154 return 155 } 156 157 // Create the defrag transaction. 158 txnSet, err := w.managedCreateDefragTransaction() 159 defer func() { 160 if err == nil { 161 return 162 } 163 w.mu.Lock() 164 defer w.mu.Unlock() 165 for _, txn := range txnSet { 166 for _, sci := range txn.SiacoinInputs { 167 dbDeleteSpentOutput(w.dbTx, types.OutputID(sci.ParentID)) 168 } 169 } 170 }() 171 if err == errDefragNotNeeded { 172 // begin 173 return 174 } else if err != nil { 175 w.log.Println("WARN: couldn't create defrag transaction:", err) 176 return 177 } 178 179 if w.deps.Disrupt("DefragInterrupted") { 180 err = errors.New("defrag was interrupted (DefragInterrupted)") 181 return 182 } 183 // Submit the defrag to the transaction pool. 184 err = w.tpool.AcceptTransactionSet(txnSet) 185 if err != nil { 186 w.log.Println("WARN: defrag transaction was rejected:", err) 187 return 188 } 189 w.log.Println("Submitting a transaction set to defragment the wallet's outputs, IDs:") 190 for _, txn := range txnSet { 191 w.log.Println("Wallet defrag: \t", txn.ID()) 192 } 193 }