decred.org/dcrwallet/v3@v3.1.0/ticketbuyer/tb.go (about) 1 // Copyright (c) 2018-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 ticketbuyer 6 7 import ( 8 "context" 9 "net" 10 "runtime/trace" 11 "sync" 12 13 "decred.org/dcrwallet/v3/errors" 14 "decred.org/dcrwallet/v3/internal/vsp" 15 "decred.org/dcrwallet/v3/wallet" 16 "github.com/decred/dcrd/dcrutil/v4" 17 "github.com/decred/dcrd/txscript/v4/stdaddr" 18 "github.com/decred/dcrd/wire" 19 ) 20 21 const minconf = 1 22 23 // Config modifies the behavior of TB. 24 type Config struct { 25 BuyTickets bool 26 27 // Account to buy tickets from 28 Account uint32 29 30 // Account to derive voting addresses from; overridden by VotingAddr 31 VotingAccount uint32 32 33 // Minimum amount to maintain in purchasing account 34 Maintain dcrutil.Amount 35 36 // Address to assign voting rights; overrides VotingAccount 37 VotingAddr stdaddr.StakeAddress 38 39 // Commitment address for stakepool fees 40 PoolFeeAddr stdaddr.StakeAddress 41 42 // Stakepool fee percentage (between 0-100) 43 PoolFees float64 44 45 // Limit maximum number of purchased tickets per block 46 Limit int 47 48 // CSPP-related options 49 CSPPServer string 50 DialCSPPServer func(ctx context.Context, network, addr string) (net.Conn, error) 51 MixedAccount uint32 52 MixedAccountBranch uint32 53 TicketSplitAccount uint32 54 ChangeAccount uint32 55 MixChange bool 56 57 // VSP client 58 VSP *vsp.Client 59 } 60 61 // TB is an automated ticket buyer, buying as many tickets as possible given an 62 // account's available balance. TB may be configured to buy tickets for any 63 // arbitrary voting address or (optional) stakepool. 64 type TB struct { 65 wallet *wallet.Wallet 66 67 cfg Config 68 mu sync.Mutex 69 } 70 71 // New returns a new TB to buy tickets from a wallet using the default config. 72 func New(w *wallet.Wallet) *TB { 73 return &TB{wallet: w} 74 } 75 76 // Run executes the ticket buyer. If the private passphrase is incorrect, or 77 // ever becomes incorrect due to a wallet passphrase change, Run exits with an 78 // errors.Passphrase error. 79 func (tb *TB) Run(ctx context.Context, passphrase []byte) error { 80 if len(passphrase) > 0 { 81 err := tb.wallet.Unlock(ctx, passphrase, nil) 82 if err != nil { 83 return err 84 } 85 } 86 87 c := tb.wallet.NtfnServer.MainTipChangedNotifications() 88 defer c.Done() 89 90 ctx, outerCancel := context.WithCancel(ctx) 91 defer outerCancel() 92 var fatal error 93 var fatalMu sync.Mutex 94 95 var nextIntervalStart, expiry int32 96 var cancels []func() 97 for { 98 select { 99 case <-ctx.Done(): 100 defer outerCancel() 101 fatalMu.Lock() 102 err := fatal 103 fatalMu.Unlock() 104 if err != nil { 105 return err 106 } 107 return ctx.Err() 108 case n := <-c.C: 109 if len(n.AttachedBlocks) == 0 { 110 continue 111 } 112 113 tip := n.AttachedBlocks[len(n.AttachedBlocks)-1] 114 w := tb.wallet 115 116 // Don't perform any actions while transactions are not synced through 117 // the tip block. 118 rp, err := w.RescanPoint(ctx) 119 if err != nil { 120 return err 121 } 122 if rp != nil { 123 log.Debugf("Skipping autobuyer actions: transactions are not synced") 124 continue 125 } 126 127 tipHeader, err := w.BlockHeader(ctx, tip) 128 if err != nil { 129 log.Error(err) 130 continue 131 } 132 height := int32(tipHeader.Height) 133 134 // Cancel any ongoing ticket purchases which are buying 135 // at an old ticket price or are no longer able to 136 // create mined tickets the window. 137 if height+2 >= nextIntervalStart { 138 for i, cancel := range cancels { 139 cancel() 140 cancels[i] = nil 141 } 142 cancels = cancels[:0] 143 144 intervalSize := int32(w.ChainParams().StakeDiffWindowSize) 145 currentInterval := height / intervalSize 146 nextIntervalStart = (currentInterval + 1) * intervalSize 147 148 // Skip this purchase when no more tickets may be purchased in the interval and 149 // the next sdiff is unknown. The earliest any ticket may be mined is two 150 // blocks from now, with the next block containing the split transaction 151 // that the ticket purchase spends. 152 if height+2 == nextIntervalStart { 153 log.Debugf("Skipping purchase: next sdiff interval starts soon") 154 continue 155 } 156 // Set expiry to prevent tickets from being mined in the next 157 // sdiff interval. When the next block begins the new interval, 158 // the ticket is being purchased for the next interval; therefore 159 // increment expiry by a full sdiff window size to prevent it 160 // being mined in the interval after the next. 161 expiry = nextIntervalStart 162 if height+1 == nextIntervalStart { 163 expiry += intervalSize 164 } 165 } 166 167 // Read config 168 tb.mu.Lock() 169 cfg := tb.cfg 170 tb.mu.Unlock() 171 172 multiple := 1 173 if cfg.CSPPServer != "" { 174 multiple = cfg.Limit 175 cfg.Limit = 1 176 } 177 178 cancelCtx, cancel := context.WithCancel(ctx) 179 cancels = append(cancels, cancel) 180 buyTickets := func() { 181 err := tb.buy(cancelCtx, passphrase, tipHeader, expiry, &cfg) 182 if err != nil { 183 switch { 184 // silence these errors 185 case errors.Is(err, errors.InsufficientBalance): 186 case errors.Is(err, context.Canceled): 187 case errors.Is(err, context.DeadlineExceeded): 188 default: 189 log.Errorf("Ticket purchasing failed: %v", err) 190 } 191 if errors.Is(err, errors.Passphrase) { 192 fatalMu.Lock() 193 fatal = err 194 fatalMu.Unlock() 195 outerCancel() 196 } 197 } 198 } 199 for i := 0; cfg.BuyTickets && i < multiple; i++ { 200 go buyTickets() 201 } 202 go func() { 203 err := tb.mixChange(ctx, &cfg) 204 if err != nil { 205 log.Error(err) 206 } 207 }() 208 } 209 } 210 } 211 212 func (tb *TB) buy(ctx context.Context, passphrase []byte, tip *wire.BlockHeader, expiry int32, 213 cfg *Config) error { 214 ctx, task := trace.NewTask(ctx, "ticketbuyer.buy") 215 defer task.End() 216 217 tb.mu.Lock() 218 buyTickets := tb.cfg.BuyTickets 219 tb.mu.Unlock() 220 if !buyTickets { 221 return nil 222 } 223 224 w := tb.wallet 225 226 // Unable to publish any transactions if the network backend is unset. 227 n, err := w.NetworkBackend() 228 if err != nil { 229 return err 230 } 231 232 if len(passphrase) > 0 { 233 // Ensure wallet is unlocked with the current passphrase. If the passphase 234 // is changed, the Run exits and TB must be restarted with the new 235 // passphrase. 236 err = w.Unlock(ctx, passphrase, nil) 237 if err != nil { 238 return err 239 } 240 } 241 242 // Read config 243 account := cfg.Account 244 maintain := cfg.Maintain 245 votingAddr := cfg.VotingAddr 246 poolFeeAddr := cfg.PoolFeeAddr 247 poolFees := cfg.PoolFees 248 limit := cfg.Limit 249 csppServer := cfg.CSPPServer 250 dialCSPPServer := cfg.DialCSPPServer 251 votingAccount := cfg.VotingAccount 252 mixedAccount := cfg.MixedAccount 253 mixedBranch := cfg.MixedAccountBranch 254 splitAccount := cfg.TicketSplitAccount 255 changeAccount := cfg.ChangeAccount 256 257 sdiff, err := w.NextStakeDifficultyAfterHeader(ctx, tip) 258 if err != nil { 259 return err 260 } 261 262 // Determine how many tickets to buy 263 var buy int 264 if maintain != 0 { 265 bal, err := w.AccountBalance(ctx, account, minconf) 266 if err != nil { 267 return err 268 } 269 spendable := bal.Spendable 270 if spendable < maintain { 271 log.Debugf("Skipping purchase: low available balance") 272 return nil 273 } 274 spendable -= maintain 275 buy = int(spendable / sdiff) 276 if buy == 0 { 277 log.Debugf("Skipping purchase: low available balance") 278 return nil 279 } 280 max := int(w.ChainParams().MaxFreshStakePerBlock) 281 if buy > max { 282 buy = max 283 } 284 } else { 285 buy = int(w.ChainParams().MaxFreshStakePerBlock) 286 } 287 if limit == 0 && csppServer != "" { 288 buy = 1 289 } else if limit > 0 && buy > limit { 290 buy = limit 291 } 292 293 purchaseTicketReq := &wallet.PurchaseTicketsRequest{ 294 Count: buy, 295 SourceAccount: account, 296 VotingAddress: votingAddr, 297 MinConf: minconf, 298 Expiry: expiry, 299 300 // CSPP 301 CSPPServer: csppServer, 302 DialCSPPServer: dialCSPPServer, 303 VotingAccount: votingAccount, 304 MixedAccount: mixedAccount, 305 MixedAccountBranch: mixedBranch, 306 MixedSplitAccount: splitAccount, 307 ChangeAccount: changeAccount, 308 309 // VSPs 310 VSPAddress: poolFeeAddr, 311 VSPFees: poolFees, 312 } 313 // If VSP is configured, we need to set the methods for vsp fee processment. 314 if tb.cfg.VSP != nil { 315 purchaseTicketReq.VSPFeePaymentProcess = tb.cfg.VSP.Process 316 purchaseTicketReq.VSPFeeProcess = tb.cfg.VSP.FeePercentage 317 } 318 tix, err := w.PurchaseTickets(ctx, n, purchaseTicketReq) 319 if tix != nil { 320 for _, hash := range tix.TicketHashes { 321 log.Infof("Purchased ticket %v at stake difficulty %v", hash, sdiff) 322 } 323 } 324 return err 325 } 326 327 // AccessConfig runs f with the current config passed as a parameter. The 328 // config is protected by a mutex and this function is safe for concurrent 329 // access to read or modify the config. It is unsafe to leak a pointer to the 330 // config, but a copy of *cfg is legal. 331 func (tb *TB) AccessConfig(f func(cfg *Config)) { 332 tb.mu.Lock() 333 f(&tb.cfg) 334 tb.mu.Unlock() 335 } 336 337 func (tb *TB) mixChange(ctx context.Context, cfg *Config) error { 338 // Read config 339 dial := cfg.DialCSPPServer 340 csppServer := cfg.CSPPServer 341 mixedAccount := cfg.MixedAccount 342 mixedBranch := cfg.MixedAccountBranch 343 changeAccount := cfg.ChangeAccount 344 mixChange := cfg.MixChange 345 346 if !mixChange || csppServer == "" { 347 return nil 348 } 349 350 ctx, task := trace.NewTask(ctx, "ticketbuyer.mixChange") 351 defer task.End() 352 353 return tb.wallet.MixAccount(ctx, dial, csppServer, changeAccount, mixedAccount, mixedBranch) 354 }