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 }