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