github.com/btcsuite/btcd@v0.24.0/integration/rpctest/rpc_harness.go (about) 1 // Copyright (c) 2016-2017 The btcsuite 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 rpctest 6 7 import ( 8 "fmt" 9 "net" 10 "os" 11 "path/filepath" 12 "strconv" 13 "sync" 14 "sync/atomic" 15 "testing" 16 "time" 17 18 "github.com/btcsuite/btcd/btcutil" 19 "github.com/btcsuite/btcd/chaincfg" 20 "github.com/btcsuite/btcd/chaincfg/chainhash" 21 "github.com/btcsuite/btcd/rpcclient" 22 "github.com/btcsuite/btcd/wire" 23 ) 24 25 const ( 26 // These constants define the minimum and maximum p2p and rpc port 27 // numbers used by a test harness. The min port is inclusive while the 28 // max port is exclusive. 29 minPeerPort = 10000 30 maxPeerPort = 35000 31 minRPCPort = maxPeerPort 32 maxRPCPort = 60000 33 34 // BlockVersion is the default block version used when generating 35 // blocks. 36 BlockVersion = 4 37 38 // DefaultMaxConnectionRetries is the default number of times we re-try 39 // to connect to the node after starting it. 40 DefaultMaxConnectionRetries = 20 41 42 // DefaultConnectionRetryTimeout is the default duration we wait between 43 // two connection attempts. 44 DefaultConnectionRetryTimeout = 50 * time.Millisecond 45 ) 46 47 var ( 48 // current number of active test nodes. 49 numTestInstances = 0 50 51 // testInstances is a private package-level slice used to keep track of 52 // all active test harnesses. This global can be used to perform 53 // various "joins", shutdown several active harnesses after a test, 54 // etc. 55 testInstances = make(map[string]*Harness) 56 57 // Used to protest concurrent access to above declared variables. 58 harnessStateMtx sync.RWMutex 59 60 // ListenAddressGenerator is a function that is used to generate two 61 // listen addresses (host:port), one for the P2P listener and one for 62 // the RPC listener. This is exported to allow overwriting of the 63 // default behavior which isn't very concurrency safe (just selecting 64 // a random port can produce collisions and therefore flakes). 65 ListenAddressGenerator = generateListeningAddresses 66 67 // defaultNodePort is the start of the range for listening ports of 68 // harness nodes. Ports are monotonically increasing starting from this 69 // number and are determined by the results of nextAvailablePort(). 70 defaultNodePort uint32 = 8333 71 72 // ListenerFormat is the format string that is used to generate local 73 // listener addresses. 74 ListenerFormat = "127.0.0.1:%d" 75 76 // lastPort is the last port determined to be free for use by a new 77 // node. It should be used atomically. 78 lastPort uint32 = defaultNodePort 79 ) 80 81 // HarnessTestCase represents a test-case which utilizes an instance of the 82 // Harness to exercise functionality. 83 type HarnessTestCase func(r *Harness, t *testing.T) 84 85 // Harness fully encapsulates an active btcd process to provide a unified 86 // platform for creating rpc driven integration tests involving btcd. The 87 // active btcd node will typically be run in simnet mode in order to allow for 88 // easy generation of test blockchains. The active btcd process is fully 89 // managed by Harness, which handles the necessary initialization, and teardown 90 // of the process along with any temporary directories created as a result. 91 // Multiple Harness instances may be run concurrently, in order to allow for 92 // testing complex scenarios involving multiple nodes. The harness also 93 // includes an in-memory wallet to streamline various classes of tests. 94 type Harness struct { 95 // ActiveNet is the parameters of the blockchain the Harness belongs 96 // to. 97 ActiveNet *chaincfg.Params 98 99 // MaxConnRetries is the maximum number of times we re-try to connect to 100 // the node after starting it. 101 MaxConnRetries int 102 103 // ConnectionRetryTimeout is the duration we wait between two connection 104 // attempts. 105 ConnectionRetryTimeout time.Duration 106 107 Client *rpcclient.Client 108 BatchClient *rpcclient.Client 109 node *node 110 handlers *rpcclient.NotificationHandlers 111 112 wallet *memWallet 113 114 testNodeDir string 115 nodeNum int 116 117 sync.Mutex 118 } 119 120 // New creates and initializes new instance of the rpc test harness. 121 // Optionally, websocket handlers and a specified configuration may be passed. 122 // In the case that a nil config is passed, a default configuration will be 123 // used. If a custom btcd executable is specified, it will be used to start the 124 // harness node. Otherwise a new binary is built on demand. 125 // 126 // NOTE: This function is safe for concurrent access. 127 func New(activeNet *chaincfg.Params, handlers *rpcclient.NotificationHandlers, 128 extraArgs []string, customExePath string) (*Harness, error) { 129 130 harnessStateMtx.Lock() 131 defer harnessStateMtx.Unlock() 132 133 // Add a flag for the appropriate network type based on the provided 134 // chain params. 135 switch activeNet.Net { 136 case wire.MainNet: 137 // No extra flags since mainnet is the default 138 case wire.TestNet3: 139 extraArgs = append(extraArgs, "--testnet") 140 case wire.TestNet: 141 extraArgs = append(extraArgs, "--regtest") 142 case wire.SimNet: 143 extraArgs = append(extraArgs, "--simnet") 144 default: 145 return nil, fmt.Errorf("rpctest.New must be called with one " + 146 "of the supported chain networks") 147 } 148 149 testDir, err := baseDir() 150 if err != nil { 151 return nil, err 152 } 153 154 nodeTestData, err := os.MkdirTemp(testDir, "rpc-node") 155 if err != nil { 156 return nil, err 157 } 158 159 certFile := filepath.Join(nodeTestData, "rpc.cert") 160 keyFile := filepath.Join(nodeTestData, "rpc.key") 161 if err := genCertPair(certFile, keyFile); err != nil { 162 return nil, err 163 } 164 165 wallet, err := newMemWallet(activeNet, uint32(numTestInstances)) 166 if err != nil { 167 return nil, err 168 } 169 170 miningAddr := fmt.Sprintf("--miningaddr=%s", wallet.coinbaseAddr) 171 extraArgs = append(extraArgs, miningAddr) 172 173 config, err := newConfig( 174 nodeTestData, certFile, keyFile, extraArgs, customExePath, 175 ) 176 if err != nil { 177 return nil, err 178 } 179 180 // Generate p2p+rpc listening addresses. 181 config.listen, config.rpcListen = ListenAddressGenerator() 182 183 // Create the testing node bounded to the simnet. 184 node, err := newNode(config, nodeTestData) 185 if err != nil { 186 return nil, err 187 } 188 189 nodeNum := numTestInstances 190 numTestInstances++ 191 192 if handlers == nil { 193 handlers = &rpcclient.NotificationHandlers{} 194 } 195 196 // If a handler for the OnFilteredBlock{Connected,Disconnected} callback 197 // callback has already been set, then create a wrapper callback which 198 // executes both the currently registered callback and the mem wallet's 199 // callback. 200 if handlers.OnFilteredBlockConnected != nil { 201 obc := handlers.OnFilteredBlockConnected 202 handlers.OnFilteredBlockConnected = func(height int32, header *wire.BlockHeader, filteredTxns []*btcutil.Tx) { 203 wallet.IngestBlock(height, header, filteredTxns) 204 obc(height, header, filteredTxns) 205 } 206 } else { 207 // Otherwise, we can claim the callback ourselves. 208 handlers.OnFilteredBlockConnected = wallet.IngestBlock 209 } 210 if handlers.OnFilteredBlockDisconnected != nil { 211 obd := handlers.OnFilteredBlockDisconnected 212 handlers.OnFilteredBlockDisconnected = func(height int32, header *wire.BlockHeader) { 213 wallet.UnwindBlock(height, header) 214 obd(height, header) 215 } 216 } else { 217 handlers.OnFilteredBlockDisconnected = wallet.UnwindBlock 218 } 219 220 h := &Harness{ 221 handlers: handlers, 222 node: node, 223 MaxConnRetries: DefaultMaxConnectionRetries, 224 ConnectionRetryTimeout: DefaultConnectionRetryTimeout, 225 testNodeDir: nodeTestData, 226 ActiveNet: activeNet, 227 nodeNum: nodeNum, 228 wallet: wallet, 229 } 230 231 // Track this newly created test instance within the package level 232 // global map of all active test instances. 233 testInstances[h.testNodeDir] = h 234 235 return h, nil 236 } 237 238 // SetUp initializes the rpc test state. Initialization includes: starting up a 239 // simnet node, creating a websockets client and connecting to the started 240 // node, and finally: optionally generating and submitting a testchain with a 241 // configurable number of mature coinbase outputs coinbase outputs. 242 // 243 // NOTE: This method and TearDown should always be called from the same 244 // goroutine as they are not concurrent safe. 245 func (h *Harness) SetUp(createTestChain bool, numMatureOutputs uint32) error { 246 // Start the btcd node itself. This spawns a new process which will be 247 // managed 248 if err := h.node.start(); err != nil { 249 return fmt.Errorf("error starting node: %w", err) 250 } 251 if err := h.connectRPCClient(); err != nil { 252 return fmt.Errorf("error connecting RPC client: %w", err) 253 } 254 255 h.wallet.Start() 256 257 // Filter transactions that pay to the coinbase associated with the 258 // wallet. 259 filterAddrs := []btcutil.Address{h.wallet.coinbaseAddr} 260 if err := h.Client.LoadTxFilter(true, filterAddrs, nil); err != nil { 261 return err 262 } 263 264 // Ensure btcd properly dispatches our registered call-back for each new 265 // block. Otherwise, the memWallet won't function properly. 266 if err := h.Client.NotifyBlocks(); err != nil { 267 return err 268 } 269 270 // Create a test chain with the desired number of mature coinbase 271 // outputs. 272 if createTestChain && numMatureOutputs != 0 { 273 coinbaseMaturity := uint32(h.ActiveNet.CoinbaseMaturity) 274 numToGenerate := coinbaseMaturity + numMatureOutputs 275 _, err := h.Client.Generate(numToGenerate) 276 if err != nil { 277 return err 278 } 279 } 280 281 // Block until the wallet has fully synced up to the tip of the main 282 // chain. 283 _, height, err := h.Client.GetBestBlock() 284 if err != nil { 285 return err 286 } 287 ticker := time.NewTicker(time.Millisecond * 100) 288 for range ticker.C { 289 walletHeight := h.wallet.SyncedHeight() 290 if walletHeight == height { 291 break 292 } 293 } 294 ticker.Stop() 295 296 return nil 297 } 298 299 // tearDown stops the running rpc test instance. All created processes are 300 // killed, and temporary directories removed. 301 // 302 // This function MUST be called with the harness state mutex held (for writes). 303 func (h *Harness) tearDown() error { 304 if h.Client != nil { 305 h.Client.Shutdown() 306 } 307 308 if h.BatchClient != nil { 309 h.BatchClient.Shutdown() 310 } 311 312 if err := h.node.shutdown(); err != nil { 313 return err 314 } 315 316 if err := os.RemoveAll(h.testNodeDir); err != nil { 317 return err 318 } 319 320 delete(testInstances, h.testNodeDir) 321 322 return nil 323 } 324 325 // TearDown stops the running rpc test instance. All created processes are 326 // killed, and temporary directories removed. 327 // 328 // NOTE: This method and SetUp should always be called from the same goroutine 329 // as they are not concurrent safe. 330 func (h *Harness) TearDown() error { 331 harnessStateMtx.Lock() 332 defer harnessStateMtx.Unlock() 333 334 return h.tearDown() 335 } 336 337 // connectRPCClient attempts to establish an RPC connection to the created btcd 338 // process belonging to this Harness instance. If the initial connection 339 // attempt fails, this function will retry h.maxConnRetries times, backing off 340 // the time between subsequent attempts. If after h.maxConnRetries attempts, 341 // we're not able to establish a connection, this function returns with an 342 // error. 343 func (h *Harness) connectRPCClient() error { 344 var client, batchClient *rpcclient.Client 345 var err error 346 347 rpcConf := h.node.config.rpcConnConfig() 348 batchConf := h.node.config.rpcConnConfig() 349 batchConf.HTTPPostMode = true 350 for i := 0; i < h.MaxConnRetries; i++ { 351 fail := false 352 timeout := time.Duration(i) * h.ConnectionRetryTimeout 353 if client == nil { 354 client, err = rpcclient.New(&rpcConf, h.handlers) 355 if err != nil { 356 time.Sleep(timeout) 357 fail = true 358 } 359 } 360 if batchClient == nil { 361 batchClient, err = rpcclient.NewBatch(&batchConf) 362 if err != nil { 363 time.Sleep(timeout) 364 fail = true 365 } 366 } 367 if !fail { 368 break 369 } 370 } 371 372 if client == nil || batchClient == nil { 373 return fmt.Errorf("connection timeout, tried %d times with "+ 374 "timeout %v, last err: %w", h.MaxConnRetries, 375 h.ConnectionRetryTimeout, err) 376 } 377 378 h.Client = client 379 h.wallet.SetRPCClient(client) 380 h.BatchClient = batchClient 381 return nil 382 } 383 384 // NewAddress returns a fresh address spendable by the Harness' internal 385 // wallet. 386 // 387 // This function is safe for concurrent access. 388 func (h *Harness) NewAddress() (btcutil.Address, error) { 389 return h.wallet.NewAddress() 390 } 391 392 // ConfirmedBalance returns the confirmed balance of the Harness' internal 393 // wallet. 394 // 395 // This function is safe for concurrent access. 396 func (h *Harness) ConfirmedBalance() btcutil.Amount { 397 return h.wallet.ConfirmedBalance() 398 } 399 400 // SendOutputs creates, signs, and finally broadcasts a transaction spending 401 // the harness' available mature coinbase outputs creating new outputs 402 // according to targetOutputs. 403 // 404 // This function is safe for concurrent access. 405 func (h *Harness) SendOutputs(targetOutputs []*wire.TxOut, 406 feeRate btcutil.Amount) (*chainhash.Hash, error) { 407 408 return h.wallet.SendOutputs(targetOutputs, feeRate) 409 } 410 411 // SendOutputsWithoutChange creates and sends a transaction that pays to the 412 // specified outputs while observing the passed fee rate and ignoring a change 413 // output. The passed fee rate should be expressed in sat/b. 414 // 415 // This function is safe for concurrent access. 416 func (h *Harness) SendOutputsWithoutChange(targetOutputs []*wire.TxOut, 417 feeRate btcutil.Amount) (*chainhash.Hash, error) { 418 419 return h.wallet.SendOutputsWithoutChange(targetOutputs, feeRate) 420 } 421 422 // CreateTransaction returns a fully signed transaction paying to the specified 423 // outputs while observing the desired fee rate. The passed fee rate should be 424 // expressed in satoshis-per-byte. The transaction being created can optionally 425 // include a change output indicated by the change boolean. Any unspent outputs 426 // selected as inputs for the crafted transaction are marked as unspendable in 427 // order to avoid potential double-spends by future calls to this method. If the 428 // created transaction is cancelled for any reason then the selected inputs MUST 429 // be freed via a call to UnlockOutputs. Otherwise, the locked inputs won't be 430 // returned to the pool of spendable outputs. 431 // 432 // This function is safe for concurrent access. 433 func (h *Harness) CreateTransaction(targetOutputs []*wire.TxOut, 434 feeRate btcutil.Amount, change bool) (*wire.MsgTx, error) { 435 436 return h.wallet.CreateTransaction(targetOutputs, feeRate, change) 437 } 438 439 // UnlockOutputs unlocks any outputs which were previously marked as 440 // unspendabe due to being selected to fund a transaction via the 441 // CreateTransaction method. 442 // 443 // This function is safe for concurrent access. 444 func (h *Harness) UnlockOutputs(inputs []*wire.TxIn) { 445 h.wallet.UnlockOutputs(inputs) 446 } 447 448 // RPCConfig returns the harnesses current rpc configuration. This allows other 449 // potential RPC clients created within tests to connect to a given test 450 // harness instance. 451 func (h *Harness) RPCConfig() rpcclient.ConnConfig { 452 return h.node.config.rpcConnConfig() 453 } 454 455 // P2PAddress returns the harness' P2P listening address. This allows potential 456 // peers (such as SPV peers) created within tests to connect to a given test 457 // harness instance. 458 func (h *Harness) P2PAddress() string { 459 return h.node.config.listen 460 } 461 462 // GenerateAndSubmitBlock creates a block whose contents include the passed 463 // transactions and submits it to the running simnet node. For generating 464 // blocks with only a coinbase tx, callers can simply pass nil instead of 465 // transactions to be mined. Additionally, a custom block version can be set by 466 // the caller. A blockVersion of -1 indicates that the current default block 467 // version should be used. An uninitialized time.Time should be used for the 468 // blockTime parameter if one doesn't wish to set a custom time. 469 // 470 // This function is safe for concurrent access. 471 func (h *Harness) GenerateAndSubmitBlock(txns []*btcutil.Tx, blockVersion int32, 472 blockTime time.Time) (*btcutil.Block, error) { 473 return h.GenerateAndSubmitBlockWithCustomCoinbaseOutputs(txns, 474 blockVersion, blockTime, []wire.TxOut{}) 475 } 476 477 // GenerateAndSubmitBlockWithCustomCoinbaseOutputs creates a block whose 478 // contents include the passed coinbase outputs and transactions and submits 479 // it to the running simnet node. For generating blocks with only a coinbase tx, 480 // callers can simply pass nil instead of transactions to be mined. 481 // Additionally, a custom block version can be set by the caller. A blockVersion 482 // of -1 indicates that the current default block version should be used. An 483 // uninitialized time.Time should be used for the blockTime parameter if one 484 // doesn't wish to set a custom time. The mineTo list of outputs will be added 485 // to the coinbase; this is not checked for correctness until the block is 486 // submitted; thus, it is the caller's responsibility to ensure that the outputs 487 // are correct. If the list is empty, the coinbase reward goes to the wallet 488 // managed by the Harness. 489 // 490 // This function is safe for concurrent access. 491 func (h *Harness) GenerateAndSubmitBlockWithCustomCoinbaseOutputs( 492 txns []*btcutil.Tx, blockVersion int32, blockTime time.Time, 493 mineTo []wire.TxOut) (*btcutil.Block, error) { 494 495 h.Lock() 496 defer h.Unlock() 497 498 if blockVersion == -1 { 499 blockVersion = BlockVersion 500 } 501 502 prevBlockHash, prevBlockHeight, err := h.Client.GetBestBlock() 503 if err != nil { 504 return nil, err 505 } 506 mBlock, err := h.Client.GetBlock(prevBlockHash) 507 if err != nil { 508 return nil, err 509 } 510 prevBlock := btcutil.NewBlock(mBlock) 511 prevBlock.SetHeight(prevBlockHeight) 512 513 // Create a new block including the specified transactions 514 newBlock, err := CreateBlock(prevBlock, txns, blockVersion, 515 blockTime, h.wallet.coinbaseAddr, mineTo, h.ActiveNet) 516 if err != nil { 517 return nil, err 518 } 519 520 // Submit the block to the simnet node. 521 if err := h.Client.SubmitBlock(newBlock, nil); err != nil { 522 return nil, err 523 } 524 525 return newBlock, nil 526 } 527 528 // generateListeningAddresses is a function that returns two listener 529 // addresses with unique ports and should be used to overwrite rpctest's 530 // default generator which is prone to use colliding ports. 531 func generateListeningAddresses() (string, string) { 532 return fmt.Sprintf(ListenerFormat, NextAvailablePort()), 533 fmt.Sprintf(ListenerFormat, NextAvailablePort()) 534 } 535 536 // NextAvailablePort returns the first port that is available for listening by 537 // a new node. It panics if no port is found and the maximum available TCP port 538 // is reached. 539 func NextAvailablePort() int { 540 port := atomic.AddUint32(&lastPort, 1) 541 for port < 65535 { 542 // If there are no errors while attempting to listen on this 543 // port, close the socket and return it as available. While it 544 // could be the case that some other process picks up this port 545 // between the time the socket is closed and it's reopened in 546 // the harness node, in practice in CI servers this seems much 547 // less likely than simply some other process already being 548 // bound at the start of the tests. 549 addr := fmt.Sprintf(ListenerFormat, port) 550 l, err := net.Listen("tcp4", addr) 551 if err == nil { 552 err := l.Close() 553 if err == nil { 554 return int(port) 555 } 556 } 557 port = atomic.AddUint32(&lastPort, 1) 558 } 559 560 // No ports available? Must be a mistake. 561 panic("no ports available for listening") 562 } 563 564 // NextAvailablePortForProcess returns the first port that is available for 565 // listening by a new node, using a lock file to make sure concurrent access for 566 // parallel tasks within the same process don't re-use the same port. It panics 567 // if no port is found and the maximum available TCP port is reached. 568 func NextAvailablePortForProcess(pid int) int { 569 lockFile := filepath.Join( 570 os.TempDir(), fmt.Sprintf("rpctest-port-pid-%d.lock", pid), 571 ) 572 timeout := time.After(time.Second) 573 574 var ( 575 lockFileHandle *os.File 576 err error 577 ) 578 for { 579 // Attempt to acquire the lock file. If it already exists, wait 580 // for a bit and retry. 581 lockFileHandle, err = os.OpenFile( 582 lockFile, os.O_CREATE|os.O_EXCL, 0600, 583 ) 584 if err == nil { 585 // Lock acquired. 586 break 587 } 588 589 // Wait for a bit and retry. 590 select { 591 case <-timeout: 592 panic("timeout waiting for lock file") 593 case <-time.After(10 * time.Millisecond): 594 } 595 } 596 597 // Release the lock file when we're done. 598 defer func() { 599 // Always close file first, Windows won't allow us to remove it 600 // otherwise. 601 _ = lockFileHandle.Close() 602 err := os.Remove(lockFile) 603 if err != nil { 604 panic(fmt.Errorf("couldn't remove lock file: %w", err)) 605 } 606 }() 607 608 portFile := filepath.Join( 609 os.TempDir(), fmt.Sprintf("rpctest-port-pid-%d", pid), 610 ) 611 port, err := os.ReadFile(portFile) 612 if err != nil { 613 if !os.IsNotExist(err) { 614 panic(fmt.Errorf("error reading port file: %w", err)) 615 } 616 port = []byte(strconv.Itoa(int(defaultNodePort))) 617 } 618 619 lastPort, err := strconv.Atoi(string(port)) 620 if err != nil { 621 panic(fmt.Errorf("error parsing port: %w", err)) 622 } 623 624 // We take the next one. 625 lastPort++ 626 for lastPort < 65535 { 627 // If there are no errors while attempting to listen on this 628 // port, close the socket and return it as available. While it 629 // could be the case that some other process picks up this port 630 // between the time the socket is closed and it's reopened in 631 // the harness node, in practice in CI servers this seems much 632 // less likely than simply some other process already being 633 // bound at the start of the tests. 634 addr := fmt.Sprintf(ListenerFormat, lastPort) 635 l, err := net.Listen("tcp4", addr) 636 if err == nil { 637 err := l.Close() 638 if err == nil { 639 err := os.WriteFile( 640 portFile, 641 []byte(strconv.Itoa(lastPort)), 0600, 642 ) 643 if err != nil { 644 panic(fmt.Errorf("error updating "+ 645 "port file: %w", err)) 646 } 647 648 return lastPort 649 } 650 } 651 lastPort++ 652 } 653 654 // No ports available? Must be a mistake. 655 panic("no ports available for listening") 656 } 657 658 // GenerateProcessUniqueListenerAddresses is a function that returns two 659 // listener addresses with unique ports per the given process id and should be 660 // used to overwrite rpctest's default generator which is prone to use colliding 661 // ports. 662 func GenerateProcessUniqueListenerAddresses(pid int) (string, string) { 663 port1 := NextAvailablePortForProcess(pid) 664 port2 := NextAvailablePortForProcess(pid) 665 return fmt.Sprintf(ListenerFormat, port1), 666 fmt.Sprintf(ListenerFormat, port2) 667 } 668 669 // baseDir is the directory path of the temp directory for all rpctest files. 670 func baseDir() (string, error) { 671 dirPath := filepath.Join(os.TempDir(), "btcd", "rpctest") 672 err := os.MkdirAll(dirPath, 0755) 673 return dirPath, err 674 }