decred.org/dcrwallet/v3@v3.1.0/wallet/mixing.go (about)

     1  // Copyright (c) 2019-2020 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package wallet
     6  
     7  import (
     8  	"context"
     9  	"crypto/rand"
    10  	"crypto/tls"
    11  	"net"
    12  	"time"
    13  
    14  	"decred.org/cspp/v2"
    15  	"decred.org/cspp/v2/coinjoin"
    16  	"decred.org/dcrwallet/v3/errors"
    17  	"decred.org/dcrwallet/v3/wallet/txrules"
    18  	"decred.org/dcrwallet/v3/wallet/txsizes"
    19  	"decred.org/dcrwallet/v3/wallet/udb"
    20  	"decred.org/dcrwallet/v3/wallet/walletdb"
    21  	"github.com/decred/dcrd/dcrutil/v4"
    22  	"github.com/decred/dcrd/wire"
    23  	"github.com/decred/go-socks/socks"
    24  	"golang.org/x/sync/errgroup"
    25  )
    26  
    27  // must be sorted large to small
    28  var splitPoints = [...]dcrutil.Amount{
    29  	1 << 36, // 687.19476736
    30  	1 << 34, // 171.79869184
    31  	1 << 32, // 042.94967296
    32  	1 << 30, // 010.73741824
    33  	1 << 28, // 002.68435456
    34  	1 << 26, // 000.67108864
    35  	1 << 24, // 000.16777216
    36  	1 << 22, // 000.04194304
    37  	1 << 20, // 000.01048576
    38  	1 << 18, // 000.00262144
    39  }
    40  
    41  func smallestMixChange(feeRate dcrutil.Amount) dcrutil.Amount {
    42  	inScriptSizes := []int{txsizes.RedeemP2PKHSigScriptSize}
    43  	outScriptSizes := []int{txsizes.P2PKHPkScriptSize}
    44  	size := txsizes.EstimateSerializeSizeFromScriptSizes(
    45  		inScriptSizes, outScriptSizes, 0)
    46  	fee := txrules.FeeForSerializeSize(feeRate, size)
    47  	return fee + splitPoints[len(splitPoints)-1]
    48  }
    49  
    50  type mixSemaphores struct {
    51  	splitSems [len(splitPoints)]chan struct{}
    52  }
    53  
    54  func newMixSemaphores(n int) mixSemaphores {
    55  	var m mixSemaphores
    56  	for i := range m.splitSems {
    57  		m.splitSems[i] = make(chan struct{}, n)
    58  	}
    59  	return m
    60  }
    61  
    62  var (
    63  	errNoSplitDenomination = errors.New("no suitable split denomination")
    64  	errThrottledMixRequest = errors.New("throttled mix request for split denomination")
    65  )
    66  
    67  // DialFunc provides a method to dial a network connection.
    68  // If the dialed network connection is secured by TLS, TLS
    69  // configuration is provided by the method, not the caller.
    70  type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error)
    71  
    72  func (w *Wallet) MixOutput(ctx context.Context, dialTLS DialFunc, csppserver string, output *wire.OutPoint, changeAccount, mixAccount, mixBranch uint32) error {
    73  	op := errors.Opf("wallet.MixOutput(%v)", output)
    74  
    75  	sdiff, err := w.NextStakeDifficulty(ctx)
    76  	if err != nil {
    77  		return errors.E(op, err)
    78  	}
    79  
    80  	w.lockedOutpointMu.Lock()
    81  	if _, exists := w.lockedOutpoints[outpoint{output.Hash, output.Index}]; exists {
    82  		w.lockedOutpointMu.Unlock()
    83  		err = errors.Errorf("output %v already locked", output)
    84  		return errors.E(op, err)
    85  	}
    86  
    87  	var prevScript []byte
    88  	var amount dcrutil.Amount
    89  	err = walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error {
    90  		txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey)
    91  		txDetails, err := w.txStore.TxDetails(txmgrNs, &output.Hash)
    92  		if err != nil {
    93  			return err
    94  		}
    95  		prevScript = txDetails.MsgTx.TxOut[output.Index].PkScript
    96  		amount = dcrutil.Amount(txDetails.MsgTx.TxOut[output.Index].Value)
    97  		return nil
    98  	})
    99  	if err != nil {
   100  		w.lockedOutpointMu.Unlock()
   101  		return errors.E(op, err)
   102  	}
   103  	w.lockedOutpoints[outpoint{output.Hash, output.Index}] = struct{}{}
   104  	w.lockedOutpointMu.Unlock()
   105  
   106  	defer func() {
   107  		w.lockedOutpointMu.Lock()
   108  		delete(w.lockedOutpoints, outpoint{output.Hash, output.Index})
   109  		w.lockedOutpointMu.Unlock()
   110  	}()
   111  
   112  	var i, count int
   113  	var mixValue, remValue, changeValue dcrutil.Amount
   114  	var feeRate = w.RelayFee()
   115  	var smallestMixChange = smallestMixChange(feeRate)
   116  SplitPoints:
   117  	for i = 0; i < len(splitPoints); i++ {
   118  		last := i == len(splitPoints)-1
   119  		mixValue = splitPoints[i]
   120  
   121  		// When the sdiff is more than this mixed output amount, there
   122  		// is a smaller common mixed amount with more pairing activity
   123  		// (due to CoinShuffle++ participation from ticket buyers).
   124  		// Skipping this amount and moving to the next smallest common
   125  		// mixed amount will result in quicker pairings, or pairings
   126  		// occurring at all.  The number of mixed outputs is capped to
   127  		// prevent a single mix being overwhelmingly funded by a single
   128  		// output, and to conserve memory resources.
   129  		if !last && mixValue >= sdiff {
   130  			continue
   131  		}
   132  
   133  		count = int(amount / mixValue)
   134  		if count > 4 {
   135  			count = 4
   136  		}
   137  		for ; count > 0; count-- {
   138  			remValue = amount - dcrutil.Amount(count)*mixValue
   139  			if remValue < 0 {
   140  				continue
   141  			}
   142  
   143  			// Determine required fee and change value, if possible.
   144  			// No change is ever included when mixing at the
   145  			// smallest amount.
   146  			const P2PKHv0Len = 25
   147  			inScriptSizes := []int{txsizes.RedeemP2PKHSigScriptSize}
   148  			outScriptSizes := make([]int, count)
   149  			for i := range outScriptSizes {
   150  				outScriptSizes[i] = P2PKHv0Len
   151  			}
   152  			size := txsizes.EstimateSerializeSizeFromScriptSizes(
   153  				inScriptSizes, outScriptSizes, P2PKHv0Len)
   154  			fee := txrules.FeeForSerializeSize(feeRate, size)
   155  			changeValue = remValue - fee
   156  			if last {
   157  				changeValue = 0
   158  			}
   159  			if changeValue <= 0 {
   160  				// Determine required fee without a change
   161  				// output.  A lower mix count or amount is
   162  				// required if the fee is still not payable.
   163  				size = txsizes.EstimateSerializeSizeFromScriptSizes(
   164  					inScriptSizes, outScriptSizes, 0)
   165  				fee = txrules.FeeForSerializeSize(feeRate, size)
   166  				if remValue < fee {
   167  					continue
   168  				}
   169  				changeValue = 0
   170  			}
   171  			if changeValue < smallestMixChange {
   172  				changeValue = 0
   173  			}
   174  
   175  			break SplitPoints
   176  		}
   177  	}
   178  	if i == len(splitPoints) {
   179  		err := errors.Errorf("output %v (%v): %w", output, amount, errNoSplitDenomination)
   180  		return errors.E(op, err)
   181  	}
   182  	select {
   183  	case <-ctx.Done():
   184  		return errors.E(op, ctx.Err())
   185  	case w.mixSems.splitSems[i] <- struct{}{}:
   186  		defer func() { <-w.mixSems.splitSems[i] }()
   187  	default:
   188  		return errThrottledMixRequest
   189  	}
   190  
   191  	var change *wire.TxOut
   192  	var updates []func(walletdb.ReadWriteTx) error
   193  	if changeValue > 0 {
   194  		persist := w.deferPersistReturnedChild(ctx, &updates)
   195  		const accountName = "" // not used, so can be faked.
   196  		addr, err := w.nextAddress(ctx, op, persist,
   197  			accountName, changeAccount, udb.InternalBranch, WithGapPolicyIgnore())
   198  		if err != nil {
   199  			return errors.E(op, err)
   200  		}
   201  		version, changeScript := addr.PaymentScript()
   202  		change = &wire.TxOut{
   203  			Value:    int64(changeValue),
   204  			PkScript: changeScript,
   205  			Version:  version,
   206  		}
   207  	}
   208  
   209  	const (
   210  		txVersion = 1
   211  		locktime  = 0
   212  		expiry    = 0
   213  	)
   214  	pairing := coinjoin.EncodeDesc(coinjoin.P2PKHv0, int64(mixValue), txVersion, locktime, expiry)
   215  	ses, err := cspp.NewSession(rand.Reader, debugLog, pairing, count)
   216  	if err != nil {
   217  		return errors.E(op, err)
   218  	}
   219  	var conn net.Conn
   220  	if dialTLS != nil {
   221  		conn, err = dialTLS(ctx, "tcp", csppserver)
   222  	} else {
   223  		conn, err = tls.Dial("tcp", csppserver, nil)
   224  	}
   225  	if err != nil {
   226  		return errors.E(op, err)
   227  	}
   228  	defer conn.Close()
   229  	log.Infof("Dialed CSPPServer %v -> %v", conn.LocalAddr(), conn.RemoteAddr())
   230  
   231  	log.Infof("Mixing output %v (%v)", output, amount)
   232  	cj := w.newCsppJoin(ctx, change, mixValue, mixAccount, mixBranch, count)
   233  	cj.addTxIn(prevScript, &wire.TxIn{
   234  		PreviousOutPoint: *output,
   235  		ValueIn:          int64(amount),
   236  	})
   237  	err = ses.DiceMix(ctx, conn, cj)
   238  	if err != nil {
   239  		return errors.E(op, err)
   240  	}
   241  	cjHash := cj.tx.TxHash()
   242  	log.Infof("Completed CoinShuffle++ mix of output %v in transaction %v", output, &cjHash)
   243  
   244  	var watch []wire.OutPoint
   245  	w.lockedOutpointMu.Lock()
   246  	err = walletdb.Update(ctx, w.db, func(dbtx walletdb.ReadWriteTx) error {
   247  		for _, f := range updates {
   248  			if err := f(dbtx); err != nil {
   249  				return err
   250  			}
   251  		}
   252  		rec, err := udb.NewTxRecordFromMsgTx(cj.tx, time.Now())
   253  		if err != nil {
   254  			return errors.E(op, err)
   255  		}
   256  		watch, err = w.processTransactionRecord(ctx, dbtx, rec, nil, nil)
   257  		if err != nil {
   258  			return err
   259  		}
   260  		return nil
   261  	})
   262  	w.lockedOutpointMu.Unlock()
   263  	if err != nil {
   264  		return errors.E(op, err)
   265  	}
   266  	n, _ := w.NetworkBackend()
   267  	if n != nil {
   268  		err = w.publishAndWatch(ctx, op, n, cj.tx, watch)
   269  	}
   270  	return err
   271  }
   272  
   273  // MixAccount individually mixes outputs of an account into standard
   274  // denominations, creating newly mixed outputs for a mixed account.
   275  //
   276  // Due to performance concerns of timing out in a CoinShuffle++ run, this
   277  // function may throttle how many of the outputs are mixed each call.
   278  func (w *Wallet) MixAccount(ctx context.Context, dialTLS DialFunc, csppserver string, changeAccount, mixAccount, mixBranch uint32) error {
   279  	const op errors.Op = "wallet.MixAccount"
   280  
   281  	_, tipHeight := w.MainChainTip(ctx)
   282  	w.lockedOutpointMu.Lock()
   283  	var credits []Input
   284  	err := walletdb.View(ctx, w.db, func(dbtx walletdb.ReadTx) error {
   285  		var err error
   286  		const minconf = 1
   287  		const targetAmount = 0
   288  		var minAmount = splitPoints[len(splitPoints)-1]
   289  		var maxResults = cap(w.mixSems.splitSems[0]) * len(splitPoints)
   290  		credits, err = w.findEligibleOutputsAmount(dbtx, changeAccount, minconf,
   291  			targetAmount, tipHeight, minAmount, maxResults)
   292  		return err
   293  	})
   294  	if err != nil {
   295  		w.lockedOutpointMu.Unlock()
   296  		return errors.E(op, err)
   297  	}
   298  	w.lockedOutpointMu.Unlock()
   299  
   300  	var g errgroup.Group
   301  	for i := range credits {
   302  		op := &credits[i].OutPoint
   303  		g.Go(func() error {
   304  			err := w.MixOutput(ctx, dialTLS, csppserver, op, changeAccount, mixAccount, mixBranch)
   305  			if errors.Is(err, errThrottledMixRequest) {
   306  				return nil
   307  			}
   308  			if errors.Is(err, errNoSplitDenomination) {
   309  				return nil
   310  			}
   311  			if errors.Is(err, socks.ErrPoolMaxConnections) {
   312  				return nil
   313  			}
   314  			return err
   315  		})
   316  	}
   317  	err = g.Wait()
   318  	if err != nil {
   319  		return errors.E(op, err)
   320  	}
   321  	return nil
   322  }
   323  
   324  // PossibleCoinJoin tests if a transaction may be a CSPP-mixed transaction.
   325  // It can return false positives, as one can create a tx which looks like a
   326  // coinjoin tx, although it isn't.
   327  func PossibleCoinJoin(tx *wire.MsgTx) (isMix bool, mixDenom int64, mixCount uint32) {
   328  	if len(tx.TxOut) < 3 || len(tx.TxIn) < 3 {
   329  		return false, 0, 0
   330  	}
   331  
   332  	numberOfOutputs := len(tx.TxOut)
   333  	numberOfInputs := len(tx.TxIn)
   334  
   335  	mixedOuts := make(map[int64]uint32)
   336  	scripts := make(map[string]int)
   337  	for _, o := range tx.TxOut {
   338  		scripts[string(o.PkScript)]++
   339  		if scripts[string(o.PkScript)] > 1 {
   340  			return false, 0, 0
   341  		}
   342  		val := o.Value
   343  		// Multiple zero valued outputs do not count as a coinjoin mix.
   344  		if val == 0 {
   345  			continue
   346  		}
   347  		mixedOuts[val]++
   348  	}
   349  
   350  	for val, count := range mixedOuts {
   351  		if count < 3 {
   352  			continue
   353  		}
   354  		if val > mixDenom {
   355  			mixDenom = val
   356  			mixCount = count
   357  		}
   358  
   359  		outputsWithNotSameAmount := uint32(numberOfOutputs) - count
   360  		if outputsWithNotSameAmount > uint32(numberOfInputs) {
   361  			return false, 0, 0
   362  		}
   363  	}
   364  
   365  	isMix = mixCount >= uint32(len(tx.TxOut)/2)
   366  	return
   367  }