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