diff --git a/blockchain/blockchain.go b/blockchain/blockchain.go index ec1a474026..08be4972e9 100644 --- a/blockchain/blockchain.go +++ b/blockchain/blockchain.go @@ -37,6 +37,7 @@ var ( ErrInvalidStateRoot = errors.New("invalid block state root") ErrInvalidGasUsed = errors.New("invalid block gas used") ErrInvalidReceiptsRoot = errors.New("invalid block receipts root") + ErrClosed = errors.New("blockchain is closed") ) // Blockchain is a blockchain reference @@ -46,6 +47,7 @@ type Blockchain struct { db storage.Storage // The Storage object (database) consensus Verifier executor Executor + stopped uint32 // used in executor halting config *chain.Chain // Config containing chain information genesis types.Hash // The hash of the genesis block @@ -93,6 +95,7 @@ type Verifier interface { type Executor interface { ProcessBlock(parentRoot types.Hash, block *types.Block, blockCreator types.Address) (*state.Transition, error) + Stop() } type BlockResult struct { @@ -862,6 +865,11 @@ func (b *Blockchain) executeBlockTransactions(block *types.Block) (*BlockResult, b.logger, ) + if b.isStopped() { + // execute stop, should not commit + return nil, ErrClosed + } + _, root := txn.Commit() // Append the receipts to the receipts cache @@ -1303,5 +1311,17 @@ func (b *Blockchain) GetBlockByNumber(blockNumber uint64, full bool) (*types.Blo // Close closes the DB connection func (b *Blockchain) Close() error { + b.executor.Stop() + b.stop() + + // close db at last return b.db.Close() } + +func (b *Blockchain) stop() { + atomic.StoreUint32(&b.stopped, 1) +} + +func (b *Blockchain) isStopped() bool { + return atomic.LoadUint32(&b.stopped) > 0 +} diff --git a/blockchain/testing.go b/blockchain/testing.go index 78415f573a..44ac22866c 100644 --- a/blockchain/testing.go +++ b/blockchain/testing.go @@ -298,6 +298,10 @@ func (m *mockExecutor) ProcessBlock( return nil, nil } +func (m *mockExecutor) Stop() { + // do nothing +} + func (m *mockExecutor) HookProcessBlock(fn processBlockDelegate) { m.processBlockFn = fn } diff --git a/e2e/framework/config.go b/e2e/framework/config.go index 6283ac0479..073815ec8c 100644 --- a/e2e/framework/config.go +++ b/e2e/framework/config.go @@ -36,21 +36,23 @@ type TestServerConfig struct { PremineAccts []*SrvAccount // Accounts with existing balances (genesis accounts) GenesisValidatorBalance *big.Int // Genesis the balance for the validators // List of initial staking addresses for the ValidatorSet SC with dev consensus - DevStakers []types.Address - Consensus ConsensusType // Consensus MechanismType - Bootnodes []string // Bootnode Addresses - PriceLimit *uint64 // Minimum gas price limit to enforce for acceptance into the pool - DevInterval int // Dev consensus update interval [s] - EpochSize uint64 // The epoch size in blocks for the IBFT layer - BlockGasLimit uint64 // Block gas limit - BlockGasTarget uint64 // Gas target for new blocks - ShowsLog bool // Flag specifying if logs are shown - IsPos bool // Specifies the mechanism used for IBFT (PoA / PoS) - Signer *crypto.EIP155Signer // Signer used for transactions - BridgeOwner types.Address // bridge contract owner - BridgeSigners []types.Address // bridge contract signers - IsWSEnable bool // enable websocket or not - RestoreFile string // blockchain restore file + DevStakers []types.Address + Consensus ConsensusType // Consensus MechanismType + Bootnodes []string // Bootnode Addresses + PriceLimit *uint64 // Minimum gas price limit to enforce for acceptance into the pool + DevInterval int // Dev consensus update interval [s] + EpochSize uint64 // The epoch size in blocks for the IBFT layer + BlockGasLimit uint64 // Block gas limit + BlockGasTarget uint64 // Gas target for new blocks + ShowsLog bool // Flag specifying if logs are shown + IsPos bool // Specifies the mechanism used for IBFT (PoA / PoS) + Signer *crypto.EIP155Signer // Signer used for transactions + ValidatorSetOwner types.Address // validatorset contract owner + BridgeOwner types.Address // bridge contract owner + BridgeSigners []types.Address // bridge contract signers + IsWSEnable bool // enable websocket or not + RestoreFile string // blockchain restore file + BlockTime uint64 // minimum block generation time (in s) } // DataDir returns path of data directory server uses @@ -160,6 +162,10 @@ func (t *TestServerConfig) SetEpochSize(epochSize uint64) { t.EpochSize = epochSize } +func (t *TestServerConfig) SetValidatorSetOwner(owner types.Address) { + t.ValidatorSetOwner = owner +} + func (t *TestServerConfig) SetBridgeOwner(owner types.Address) { t.BridgeOwner = owner } @@ -175,3 +181,7 @@ func (t *TestServerConfig) EnableWebSocket() { func (t *TestServerConfig) SetRestoreFile(path string) { t.RestoreFile = path } + +func (t *TestServerConfig) SetBlockTime(blockTime uint64) { + t.BlockTime = blockTime +} diff --git a/e2e/framework/helper.go b/e2e/framework/helper.go index e38ad895ea..14d7d69138 100644 --- a/e2e/framework/helper.go +++ b/e2e/framework/helper.go @@ -30,6 +30,10 @@ import ( empty "google.golang.org/protobuf/types/known/emptypb" ) +var ( + DefaultTimeout = time.Minute +) + func EthToWei(ethValue int64) *big.Int { return EthToWeiPrecise(ethValue, 18) } diff --git a/e2e/framework/ibft.go b/e2e/framework/ibft.go index 87d84c96ac..a901e220d3 100644 --- a/e2e/framework/ibft.go +++ b/e2e/framework/ibft.go @@ -83,14 +83,16 @@ func NewIBFTServersManager( } func (m *IBFTServersManager) StartServers(ctx context.Context) { - for _, srv := range m.servers { + for i, srv := range m.servers { if err := srv.Start(ctx); err != nil { + m.t.Logf("server %d failed to start: %+v", i, err) m.t.Fatal(err) } } - for _, srv := range m.servers { + for i, srv := range m.servers { if err := srv.WaitForReady(ctx); err != nil { + m.t.Logf("server %d couldn't advance block: %+v", i, err) m.t.Fatal(err) } } diff --git a/e2e/framework/testserver.go b/e2e/framework/testserver.go index bdbbe99440..558d992413 100644 --- a/e2e/framework/testserver.go +++ b/e2e/framework/testserver.go @@ -302,6 +302,11 @@ func (t *TestServer) GenerateGenesis() error { blockGasLimit := strconv.FormatUint(t.Config.BlockGasLimit, 10) args = append(args, "--block-gas-limit", blockGasLimit) + // add validatorset contract owner + if t.Config.ValidatorSetOwner != types.ZeroAddress { + args = append(args, "--validatorset-owner", t.Config.ValidatorSetOwner.String()) + } + // add bridge contract owner if t.Config.BridgeOwner != types.ZeroAddress { args = append(args, "--bridge-owner", t.Config.BridgeOwner.String()) @@ -371,6 +376,11 @@ func (t *TestServer) Start(ctx context.Context) error { args = append(args, "--block-gas-target", *types.EncodeUint64(t.Config.BlockGasTarget)) } + // block generation time, not dev consesus + if t.Config.BlockTime > 0 { + args = append(args, "--block-time", strconv.FormatUint(t.Config.BlockTime, 10)) + } + t.ReleaseReservedPorts() // Start the server diff --git a/e2e/transaction_test.go b/e2e/transaction_test.go index 7c00b8eed9..450c7f9ab9 100644 --- a/e2e/transaction_test.go +++ b/e2e/transaction_test.go @@ -188,88 +188,87 @@ func TestEthTransfer(t *testing.T) { rpcClient := srv.JSONRPC() for _, testCase := range testTable { - t.Run(testCase.name, func(t *testing.T) { - // Fetch the balances before sending - balanceSender, err := rpcClient.Eth().GetBalance( - web3.Address(testCase.sender), - web3.Latest, - ) - assert.NoError(t, err) + // Fetch the balances before sending + balanceSender, err := rpcClient.Eth().GetBalance( + web3.Address(testCase.sender), + web3.Latest, + ) + assert.NoError(t, err, testCase.name) + + balanceReceiver, err := rpcClient.Eth().GetBalance( + web3.Address(testCase.recipient), + web3.Latest, + ) + assert.NoError(t, err, testCase.name) + + // Set the preSend balances + previousSenderBalance := balanceSender + previousReceiverBalance := balanceReceiver + + // Do the transfer + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + txn := &framework.PreparedTransaction{ + From: testCase.sender, + To: &testCase.recipient, + GasPrice: big.NewInt(1048576), + Gas: 1000000, + Value: testCase.amount, + } - balanceReceiver, err := rpcClient.Eth().GetBalance( - web3.Address(testCase.recipient), - web3.Latest, + receipt, err := srv.SendRawTx(ctx, txn, testCase.senderKey) + + if testCase.shouldSucceed { + assert.NoError(t, err, testCase.name) + assert.NotNil(t, receipt, testCase.name) + } else { // When an invalid transaction is supplied, there should be no receipt. + assert.Error(t, err, testCase.name) + assert.Nil(t, receipt, testCase.name) + } + + // Fetch the balances after sending + balanceSender, err = rpcClient.Eth().GetBalance( + web3.Address(testCase.sender), + web3.Latest, + ) + assert.NoError(t, err, testCase.name) + + balanceReceiver, err = rpcClient.Eth().GetBalance( + web3.Address(testCase.recipient), + web3.Latest, + ) + assert.NoError(t, err, testCase.name) + + expectedSenderBalance := previousSenderBalance + expectedReceiverBalance := previousReceiverBalance + + if testCase.shouldSucceed { + fee := new(big.Int).Mul( + big.NewInt(int64(receipt.GasUsed)), + txn.GasPrice, ) - assert.NoError(t, err) - // Set the preSend balances - previousSenderBalance := balanceSender - previousReceiverBalance := balanceReceiver - - // Do the transfer - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - txn := &framework.PreparedTransaction{ - From: testCase.sender, - To: &testCase.recipient, - GasPrice: big.NewInt(1048576), - Gas: 1000000, - Value: testCase.amount, - } - - receipt, err := srv.SendRawTx(ctx, txn, testCase.senderKey) - - if testCase.shouldSucceed { - assert.NoError(t, err) - assert.NotNil(t, receipt) - } else { // When an invalid transaction is supplied, there should be no receipt. - assert.Error(t, err) - assert.Nil(t, receipt) - } - - // Fetch the balances after sending - balanceSender, err = rpcClient.Eth().GetBalance( - web3.Address(testCase.sender), - web3.Latest, + expectedSenderBalance = previousSenderBalance.Sub( + previousSenderBalance, + new(big.Int).Add(testCase.amount, fee), ) - assert.NoError(t, err) - balanceReceiver, err = rpcClient.Eth().GetBalance( - web3.Address(testCase.recipient), - web3.Latest, + expectedReceiverBalance = previousReceiverBalance.Add( + previousReceiverBalance, + testCase.amount, ) - assert.NoError(t, err) + } - expectedSenderBalance := previousSenderBalance - expectedReceiverBalance := previousReceiverBalance - if testCase.shouldSucceed { - fee := new(big.Int).Mul( - big.NewInt(int64(receipt.GasUsed)), - txn.GasPrice, - ) - - expectedSenderBalance = previousSenderBalance.Sub( - previousSenderBalance, - new(big.Int).Add(testCase.amount, fee), - ) - - expectedReceiverBalance = previousReceiverBalance.Add( - previousReceiverBalance, - testCase.amount, - ) - } - - // Check the balances - assert.Equalf(t, - expectedSenderBalance, - balanceSender, - "Sender balance incorrect") - assert.Equalf(t, - expectedReceiverBalance, - balanceReceiver, - "Receiver balance incorrect") - }) + // Check the balances + assert.Equalf(t, + expectedSenderBalance, + balanceSender, + testCase.name+": Sender balance incorrect") + assert.Equalf(t, + expectedReceiverBalance, + balanceReceiver, + testCase.name+": Receiver balance incorrect") } } diff --git a/e2e/txpool_test.go b/e2e/txpool_test.go index 810b2879db..6ebcc07de3 100644 --- a/e2e/txpool_test.go +++ b/e2e/txpool_test.go @@ -5,7 +5,6 @@ import ( "crypto/ecdsa" "errors" "fmt" - "io" "math/big" "sync" "testing" @@ -18,7 +17,6 @@ import ( txpoolOp "github.com/dogechain-lab/dogechain/txpool/proto" "github.com/dogechain-lab/dogechain/types" "github.com/golang/protobuf/ptypes/any" - "github.com/golang/protobuf/ptypes/empty" "github.com/stretchr/testify/assert" "github.com/umbracle/go-web3" ) @@ -28,36 +26,6 @@ var ( signer = crypto.NewEIP155Signer(100) ) -func waitForBlock(t *testing.T, srv *framework.TestServer, expectedBlocks int, index int) int64 { - t.Helper() - - systemClient := srv.Operator() - ctx, cancelFn := context.WithCancel(context.Background()) - stream, err := systemClient.Subscribe(ctx, &empty.Empty{}) - - if err != nil { - cancelFn() - t.Fatalf("Unable to subscribe to blockchain events") - } - - evnt, err := stream.Recv() - if errors.Is(err, io.EOF) { - t.Fatalf("Invalid stream close") - } - - if err != nil { - t.Fatalf("Unable to read blockchain event") - } - - if len(evnt.Added) != expectedBlocks { - t.Fatalf("Invalid number of blocks added") - } - - cancelFn() - - return evnt.Added[index].Number -} - type generateTxReqParams struct { nonce uint64 referenceAddr types.Address @@ -202,16 +170,28 @@ func TestTxPool_TransactionCoalescing(t *testing.T) { referenceKey, referenceAddr := tests.GenerateKeyAndAddr(t) defaultBalance := framework.EthToWei(10) - devInterval := 5 + fakeAddr := types.StringToAddress("0x1234") // Set up the test server - srvs := framework.NewTestServers(t, 1, func(config *framework.TestServerConfig) { - config.SetConsensus(framework.ConsensusDev) - config.SetSeal(true) - config.SetDevInterval(devInterval) - config.Premine(referenceAddr, defaultBalance) - }) - srv := srvs[0] + ibftManager := framework.NewIBFTServersManager( + t, + 1, + IBFTDirPrefix, + func(i int, config *framework.TestServerConfig) { + config.SetIBFTPoS(true) + config.SetValidatorSetOwner(fakeAddr) + config.SetBridgeOwner(fakeAddr) + config.SetBridgeSigners([]types.Address{fakeAddr}) + config.SetSeal(true) + config.Premine(referenceAddr, defaultBalance) + config.SetBlockTime(1) + }) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + ibftManager.StartServers(ctx) + + srv := ibftManager.GetServer(0) client := srv.JSONRPC() // Required default values @@ -251,19 +231,44 @@ func TestTxPool_TransactionCoalescing(t *testing.T) { return msg } + // testTransaction is a helper structure for + // keeping track of test transaction execution + type testTransaction struct { + txHash web3.Hash // the transaction hash + block *uint64 // the block the transaction was included in + } + + testTransactions := make([]*testTransaction, 0) + // Add the transactions with the following nonce order nonces := []uint64{0, 2} for i := 0; i < len(nonces); i++ { addReq := generateReq(nonces[i]) - _, addErr := clt.AddTxn(context.Background(), addReq) + ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + + addResp, addErr := clt.AddTxn(ctx, addReq) if addErr != nil { t.Fatalf("Unable to add txn, %v", addErr) } + + testTransactions = append(testTransactions, &testTransaction{ + txHash: web3.HexToHash(addResp.TxHash), + }) + + cancelFn() + } + + // Wait for the first transaction to go through + ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + + receipt, receiptErr := tests.WaitForReceipt(ctx, client.Eth(), testTransactions[0].txHash) + if receiptErr != nil { + t.Fatalf("unable to wait for receipt, %v", receiptErr) } - // Wait for the state transition to be executed - _ = waitForBlock(t, srv, 1, 0) + testTransactions[0].block = &receipt.BlockNumber // Get to account balance // Only the first tx should've gone through @@ -277,13 +282,32 @@ func TestTxPool_TransactionCoalescing(t *testing.T) { // Add the transaction with the gap nonce value addReq := generateReq(1) - _, addErr := clt.AddTxn(context.Background(), addReq) + addCtx, addCtxCn := context.WithTimeout(context.Background(), 10*time.Second) + defer addCtxCn() + + addResp, addErr := clt.AddTxn(addCtx, addReq) if addErr != nil { t.Fatalf("Unable to add txn, %v", addErr) } - // Wait for the state transition to be executed - _ = waitForBlock(t, srv, 1, 0) + testTransactions = append(testTransactions, &testTransaction{ + txHash: web3.HexToHash(addResp.TxHash), + }) + + // Start from 1 since there was previously a txn with nonce 0 + for i := 1; i < len(testTransactions); i++ { + // Wait for the first transaction to go through + ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + + receipt, receiptErr := tests.WaitForReceipt(ctx, client.Eth(), testTransactions[i].txHash) + if receiptErr != nil { + t.Fatalf("unable to wait for receipt, %v", receiptErr) + } + + testTransactions[i].block = &receipt.BlockNumber + + cancelFn() + } // Now both the added tx and the shelved tx should've gone through toAccountBalance = framework.GetAccountBalance(t, toAddress, client) @@ -292,6 +316,9 @@ func TestTxPool_TransactionCoalescing(t *testing.T) { toAccountBalance.String(), "To address balance mismatch after gap transaction", ) + + // Make sure the first transaction and the last transaction didn't get included in the same block + assert.NotEqual(t, *(testTransactions[0].block), *(testTransactions[2].block)) } type testAccount struct { diff --git a/network/server.go b/network/server.go index 8dbeafcda7..188bea1f2c 100644 --- a/network/server.go +++ b/network/server.go @@ -584,7 +584,7 @@ func (s *Server) Close() error { err := s.host.Close() s.dialQueue.Close() - if !s.config.NoDiscover { + if s.discovery != nil { s.discovery.Close() } diff --git a/server/server.go b/server/server.go index b5cc61460e..238fb2615f 100644 --- a/server/server.go +++ b/server/server.go @@ -686,9 +686,9 @@ func (s *Server) JoinPeer(rawPeerMultiaddr string) error { // Close closes the Minimal server (blockchain, networking, consensus) func (s *Server) Close() { - // Close the blockchain layer - if err := s.blockchain.Close(); err != nil { - s.logger.Error("failed to close blockchain", "err", err.Error()) + // Close the consensus layer + if err := s.consensus.Close(); err != nil { + s.logger.Error("failed to close consensus", "err", err.Error()) } // Close the networking layer @@ -696,9 +696,12 @@ func (s *Server) Close() { s.logger.Error("failed to close networking", "err", err.Error()) } - // Close the consensus layer - if err := s.consensus.Close(); err != nil { - s.logger.Error("failed to close consensus", "err", err.Error()) + // close the txpool's main loop + s.txpool.Close() + + // Close the blockchain layer + if err := s.blockchain.Close(); err != nil { + s.logger.Error("failed to close blockchain", "err", err.Error()) } // Close the state storage @@ -711,9 +714,6 @@ func (s *Server) Close() { s.logger.Error("Prometheus server shutdown error", err) } } - - // close the txpool's main loop - s.txpool.Close() } // Entry is a backend configuration entry diff --git a/state/executor.go b/state/executor.go index f114827d06..ff0213164c 100644 --- a/state/executor.go +++ b/state/executor.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "math/big" + "sync/atomic" "github.com/dogechain-lab/dogechain/chain" "github.com/dogechain-lab/dogechain/contracts/bridge" @@ -36,6 +37,7 @@ type Executor struct { runtimes []runtime.Runtime state State GetHash GetHashByNumberHelper + stopped uint32 // atomic flag for stopping PostHook func(txn *Transition) } @@ -102,6 +104,11 @@ func (e *Executor) ProcessBlock( txn.block = block for _, t := range block.Transactions { + if e.IsStopped() { + // halt more elegantly + return nil, ErrExecutionStop + } + if t.ExceedsBlockGasLimit(block.Header.GasLimit) { if err := txn.WriteFailedReceipt(t); err != nil { return nil, err @@ -118,6 +125,14 @@ func (e *Executor) ProcessBlock( return txn, nil } +func (e *Executor) IsStopped() bool { + return atomic.LoadUint32(&e.stopped) > 0 +} + +func (e *Executor) Stop() { + atomic.StoreUint32(&e.stopped, 1) +} + // StateAt returns snapshot at given root func (e *Executor) State() State { return e.state @@ -449,6 +464,7 @@ var ( ErrNotEnoughIntrinsicGas = errors.New("not enough gas supplied for intrinsic gas costs") ErrNotEnoughFunds = errors.New("not enough funds for transfer with given value") ErrAllGasUsed = errors.New("all gas used") + ErrExecutionStop = errors.New("execution stop") ) type TransitionApplicationError struct { @@ -621,7 +637,8 @@ func (t *Transition) Call2( value *big.Int, gas uint64, ) *runtime.ExecutionResult { - c := runtime.NewContractCall(1, caller, caller, to, value, gas, t.state.GetCode(to), input) + code := t.state.GetCode(to) + c := runtime.NewContractCall(1, caller, caller, to, value, gas, code, input) return t.applyCall(c, runtime.Call, t) } diff --git a/state/immutable-trie/storage.go b/state/immutable-trie/storage.go index 517157e89a..15d03473d1 100644 --- a/state/immutable-trie/storage.go +++ b/state/immutable-trie/storage.go @@ -1,6 +1,7 @@ package itrie import ( + "errors" "fmt" "github.com/dogechain-lab/dogechain/helper/hex" @@ -71,7 +72,9 @@ func (kv *KVStorage) Put(k, v []byte) { func (kv *KVStorage) Get(k []byte) ([]byte, bool) { data, err := kv.db.Get(k, nil) if err != nil { - if err.Error() == "leveldb: not found" { + if errors.Is(err, leveldb.ErrNotFound) { + return nil, false + } else if errors.Is(err, leveldb.ErrClosed) { return nil, false } else { panic(err) diff --git a/state/runtime/runtime.go b/state/runtime/runtime.go index 6e97dc04f4..0e15476b88 100644 --- a/state/runtime/runtime.go +++ b/state/runtime/runtime.go @@ -107,6 +107,7 @@ var ( ErrDepth = errors.New("max call depth exceeded") ErrExecutionReverted = errors.New("execution was reverted") ErrCodeStoreOutOfGas = errors.New("contract creation code storage out of gas") + ErrCodeEmpty = errors.New("contract code empty") ) type CallType int diff --git a/state/txn.go b/state/txn.go index 0b52c673fa..d3d11bc860 100644 --- a/state/txn.go +++ b/state/txn.go @@ -420,7 +420,10 @@ func (txn *Txn) GetCode(addr types.Address) []byte { } code, _ := txn.state.GetCode(types.BytesToHash(object.Account.CodeHash)) - txn.codeCache.Add(addr, code) + if len(code) > 0 { + // code might be empty when closed + txn.codeCache.Add(addr, code) + } return code } diff --git a/txpool/txpool.go b/txpool/txpool.go index 85eff005c5..9245c2de94 100644 --- a/txpool/txpool.go +++ b/txpool/txpool.go @@ -279,10 +279,14 @@ func (p *TxPool) Start() { select { case <-p.shutdownCh: return - case req := <-p.enqueueReqCh: - go p.handleEnqueueRequest(req) - case req := <-p.promoteReqCh: - go p.handlePromoteRequest(req) + case req, ok := <-p.enqueueReqCh: + if ok { + go p.handleEnqueueRequest(req) + } + case req, ok := <-p.promoteReqCh: + if ok { + go p.handlePromoteRequest(req) + } case _, ok := <-p.pruneAccountTicker.C: if ok { // readable go p.pruneStaleAccounts() @@ -296,7 +300,12 @@ func (p *TxPool) Start() { func (p *TxPool) Close() { p.pruneAccountTicker.Stop() p.eventManager.Close() + // stop p.shutdownCh <- struct{}{} + // close all channels + close(p.enqueueReqCh) + close(p.promoteReqCh) + close(p.shutdownCh) } // SetSigner sets the signer the pool will use