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  }