docs: public Collection<ProposalResponse> sendTransactionProposalToEndorsers(TransactionProposalRequest transactionProposalRequest, DiscoveryOptions discoveryOptions) throws ProposalException, InvalidArgumentException, ServiceDiscoveryException {
          final String chaincodeName = transactionProposalRequest.getChaincodeName() != null ? transactionProposalRequest.getChaincodeName() : transactionProposalRequest.getChaincodeID().getName();
          checkChannelState();
          if (null == transactionProposalRequest) {
              throw new InvalidArgumentException("The proposalRequest is null");
          }
          if (isNullOrEmpty(transactionProposalRequest.getFcn())) {
              throw new InvalidArgumentException("The proposalRequest's fcn is null or empty.");
          }
          if (transactionProposalRequest.getChaincodeID() == null) {
              throw new InvalidArgumentException("The proposalRequest's chaincode ID is null");
          }
          logger.debug(format("Channel %s sendTransactionProposalToEndorsers chaincode name: %s", name, chaincodeName));
  
          TransactionContext transactionContext = getTransactionContext(transactionProposalRequest.getUserContext());
          transactionContext.verify(transactionProposalRequest.doVerify());
          transactionContext.setProposalWaitTime(transactionProposalRequest.getProposalWaitTime());
  
          // Protobuf message builder
          ProposalBuilder proposalBuilder = ProposalBuilder.newBuilder();
          proposalBuilder.context(transactionContext);
          proposalBuilder.request(transactionProposalRequest);
  
          SignedProposal invokeProposal = null;
          try {
              invokeProposal = getSignedProposal(transactionContext, proposalBuilder.build());
          } catch (CryptoException e) {
              throw new InvalidArgumentException(e);
          }
          SDChaindcode sdChaindcode;
          final List<ServiceDiscoveryChaincodeCalls> serviceDiscoveryChaincodeInterests = discoveryOptions.getServiceDiscoveryChaincodeInterests();
  
          if (null != serviceDiscoveryChaincodeInterests && !serviceDiscoveryChaincodeInterests.isEmpty()) {
              final String firstname = serviceDiscoveryChaincodeInterests.get(0).getName();
              if (!firstname.equals(chaincodeName)) {
                  serviceDiscoveryChaincodeInterests.add(0, new ServiceDiscoveryChaincodeCalls(chaincodeName));
              }
              List<List<ServiceDiscoveryChaincodeCalls>> ccl = new LinkedList<>();
              ccl.add(serviceDiscoveryChaincodeInterests);
              final Map<String, SDChaindcode> sdChaindcodeMap = serviceDiscovery.discoverEndorserEndpoints(transactionContext, ccl);
              if (sdChaindcodeMap == null) {
                  throw new ServiceDiscoveryException(format("Channel %s failed doing service discovery for chaincode %s ", name, chaincodeName));
              }
              sdChaindcode = sdChaindcodeMap.get(chaincodeName);
  
          } else {
              if (discoveryOptions.forceDiscovery) {
                  logger.trace("Forcing discovery.");
                  serviceDiscovery.networkDiscovery(transactionContext, true);
              }
              sdChaindcode = serviceDiscovery.discoverEndorserEndpoint(transactionContext, chaincodeName);
          }
          logger.trace(format("Channel %s chaincode %s discovered: %s", name, chaincodeName, "" + sdChaindcode));
  
          if (null == sdChaindcode) {
              throw new ServiceDiscoveryException(format("Channel %s failed to find and endorsers for chaincode %s", name, chaincodeName));
          }
  
          if (sdChaindcode.getLayouts() == null || sdChaindcode.getLayouts().isEmpty()) {
              throw new ServiceDiscoveryException(format("Channel %s failed to find and endorsers for chaincode %s no layouts found.", name, chaincodeName));
          }
  
          SDChaindcode sdChaindcodeEndorsementCopy = new SDChaindcode(sdChaindcode); //copy. no ignored.
  
          final boolean inspectResults = discoveryOptions.inspectResults;
  
          if (sdChaindcodeEndorsementCopy.ignoreList(discoveryOptions.getIgnoreList()) < 1) { // apply ignore list
              throw new ServiceDiscoveryException("Applying ignore list reduced to no available endorser options.");
          }
  
          if (IS_TRACE_LEVEL && null != discoveryOptions.getIgnoreList() && !discoveryOptions.getIgnoreList().isEmpty()) {
              logger.trace(format("SDchaincode after ignore list: %s", sdChaindcodeEndorsementCopy));
          }
          final ServiceDiscovery.EndorsementSelector lendorsementSelector = discoveryOptions.endorsementSelector != null ?
                  discoveryOptions.endorsementSelector : this.endorsementSelector;
          try {
  
              final Map<SDEndorser, ProposalResponse> goodResponses = new HashMap<>(); // all good endorsements by endpoint
              final Map<SDEndorser, ProposalResponse> allTried = new HashMap<>(); // all tried by endpoint
  
              boolean done = false;
              int attempts = 1; //safety valve
  
              do {
                  if (IS_TRACE_LEVEL) {
                      logger.trace(format("Attempts: %d,  chaincode discovery state: %s", attempts, sdChaindcodeEndorsementCopy));
                  }
                  final SDEndorserState sdEndorserState = lendorsementSelector.endorserSelector(sdChaindcodeEndorsementCopy);
  
                  if (IS_TRACE_LEVEL) {
  
                      StringBuilder sb = new StringBuilder(1000);
                      String sep = "";
                      for (SDEndorser sdEndorser : sdEndorserState.getSdEndorsers()) {
                          sb.append(sep).append(sdEndorser);
                          sep = ", ";
                      }
  
                      logger.trace(format("Attempts: %d,  chaincode discovery state: %s. Endorser selector picked: %s. With selected endorsers: %s", attempts, sdChaindcodeEndorsementCopy.name, sdEndorserState.getPickedLayout(), sb.toString()));
  
                  }
  
                  Collection<SDEndorser> ep = sdEndorserState.getSdEndorsers();
                  ep = new ArrayList<>(ep); // just in case it's not already a copy
  
                  if (IS_TRACE_LEVEL) {
  
                      StringBuilder sb = new StringBuilder(1000);
                      String sep = "";
                      for (SDEndorser sdEndorser : ep) {
                          sb.append(sep).append(sdEndorser);
                      }
                      logger.trace(format("Channel %s, chaincode %s attempts: %d requested endorsements: %s", name, chaincodeName, attempts, sb.toString()));
                  }
  
                  //Safety check make sure the selector isn't giving back endpoints to retry
                  ep.removeIf(sdEndorser -> goodResponses.keySet().contains(sdEndorser));
  
                  if (ep.isEmpty()) { // this would be odd but lets go with it.
                      logger.debug(format("Channel %s, chaincode %s attempts: %d endorser selector returned no additional endorements needed.", name, chaincodeName, attempts));
  
                      Collection<SDEndorser> needed = sdChaindcode.meetsEndorsmentPolicy(goodResponses.keySet());
                      if (needed != null) { // means endorsment meet with those in the needed.
                          ArrayList<ProposalResponse> ret = new ArrayList<>(needed.size());
                          needed.forEach(s -> ret.add(goodResponses.get(s)));
  
                          if (IS_DEBUG_LEVEL) {
  
                              StringBuilder sb = new StringBuilder(1000);
                              String sep = "";
                              for (ProposalResponse proposalResponse : ret) {
                                  sb.append(sep).append(proposalResponse.getPeer());
                                  sep = ", ";
                              }
                              logger.debug(format("Channel %s, chaincode %s attempts: %d got all needed endorsements: %s", name, chaincodeName, attempts, sb.toString()));
                          }
  
                          return ret; // the happy path :)!
  
                      } else { //still don't have the needed endorsements.
  
                          logger.debug(format("Channel %s, chaincode %s attempts: %d missing needed endorsements", name, chaincodeName, attempts));
  
                          if (inspectResults) {
                              return allTried.values();
                          } else {
                              throw new ServiceDiscoveryException(format("Could not meet endorsement policy for chaincode %s", chaincodeName));
                          }
                      }
                  }
  
                  Map<String, Peer> lpeerEndpointMap = new HashMap<>(peerEndpointMap);
                  Map<SDEndorser, Peer> endorsers = new HashMap<>(ep.size());
                  Map<PeerExactMatch, SDEndorser> peer2sdEndorser = new HashMap<>(ep.size());
                  for (SDEndorser sdEndorser : ep) {
  
                      Peer epeer = lpeerEndpointMap.get(sdEndorser.getEndpoint());
                      if (epeer != null && !epeer.hasConnected()) {
                          // mostly because gossip may have malicious data so if we've not connected update TLS props from chaincode discovery.
                          final Properties properties = epeer.getProperties();
  
                          final byte[] bytes = combineCerts(sdEndorser.getTLSCerts(), sdEndorser.getTLSIntermediateCerts());
                          properties.put("pemBytes", bytes);
                          epeer.setProperties(properties);
  
                      } else if (null == epeer) {
                          epeer = sdPeerAddition.addPeer(new SDPeerAdditionInfo() {
  
                              @Override
                              public String getMspId() {
                                  return sdEndorser.getMspid();
                              }
  
                              @Override
                              public String getEndpoint() {
                                  return sdEndorser.getEndpoint();
                              }
  
                              @Override
                              public Channel getChannel() {
                                  return Channel.this;
                              }
  
                              @Override
                              public HFClient getClient() {
                                  return Channel.this.client;
                              }
  
                              @Override
                              public byte[][] getTLSCerts() {
  
                                  return sdEndorser.getTLSCerts().toArray(new byte[sdEndorser.getTLSCerts().size()][]);
                              }
  
                              @Override
                              public byte[][] getTLSIntermediateCerts() {
                                  return sdEndorser.getTLSIntermediateCerts().toArray(new byte[sdEndorser.getTLSIntermediateCerts().size()][]);
                              }
  
                              @Override
                              public Map<String, Peer> getEndpointMap() {
                                  return Collections.unmodifiableMap(Channel.this.peerEndpointMap);
                              }
                          });
                      }
                      endorsers.put(sdEndorser, epeer);
                      peer2sdEndorser.put(new PeerExactMatch(epeer), sdEndorser); // reverse
                  }
  
                  final Collection<ProposalResponse> proposalResponses = sendProposalToPeers(endorsers.values(), invokeProposal, transactionContext);
                  HashSet<SDEndorser> loopGood = new HashSet<>();
                  HashSet<SDEndorser> loopBad = new HashSet<>();
  
                  for (ProposalResponse proposalResponse : proposalResponses) {
                      final SDEndorser sdEndorser = peer2sdEndorser.get(new PeerExactMatch(proposalResponse.getPeer()));
                      allTried.put(sdEndorser, proposalResponse);
  
                      final ChaincodeResponse.Status status = proposalResponse.getStatus();
  
                      if (ChaincodeResponse.Status.SUCCESS.equals(status)) {
  
                          goodResponses.put(sdEndorser, proposalResponse);
                          logger.trace(format("Channel %s, chaincode %s attempts %d good endorsements: %s", name, chaincodeName, attempts, sdEndorser));
                          loopGood.add(sdEndorser);
  
                      } else {
                          logger.debug(format("Channel %s, chaincode %s attempts %d bad endorsements: %s", name, chaincodeName, attempts, sdEndorser));
                          loopBad.add(sdEndorser);
                      }
                  }
  
                  //Always check on original
                  Collection<SDEndorser> required = sdChaindcode.meetsEndorsmentPolicy(goodResponses.keySet());
                  if (required != null) {
                      ArrayList<ProposalResponse> ret = new ArrayList<>(required.size());
                      required.forEach(s -> ret.add(goodResponses.get(s)));
  
                      if (IS_DEBUG_LEVEL) {
  
                          StringBuilder sb = new StringBuilder(1000);
                          String sep = "";
                          for (ProposalResponse proposalResponse : ret) {
                              sb.append(sep).append(proposalResponse.getPeer());
                              sep = ", ";
                          }
                          logger.debug(format("Channel %s, chaincode %s got all needed endorsements: %s", name, chaincodeName, sb.toString()));
                      }
                      return ret; // the happy path :)!
  
                  } else { //still don't have the needed endorsements.
  
                      sdChaindcodeEndorsementCopy.endorsedList(loopGood); // mark the good ones in the working copy.
  
                      if (sdChaindcodeEndorsementCopy.ignoreListSDEndorser(loopBad) < 1) { // apply ignore list
                          done = true; // no more layouts
                      }
                  }
  
              } while (!done && ++attempts <= 5);
              logger.debug(format("Endorsements not achieved chaincode: %s, done: %b, attempts: %d", chaincodeName, done, attempts));
              if (inspectResults) {
                  return allTried.values();
              } else {
                  throw new ServiceDiscoveryException(format("Could not meet endorsement policy for chaincode %s", chaincodeName));
              }
          } catch (ProposalException e) {
              throw e;
  
          } catch (Exception e) {
              ProposalException exp = new ProposalException(e);
              logger.error(exp.getMessage(), exp);
              throw exp;
          }
      }