Browse Source

Add support to ElectrumX for barring servers that don't give us the data we need

pull/29/head
catbref 4 years ago
parent
commit
8707f154ee
  1. 86
      src/main/java/org/qortal/crosschain/ElectrumX.java
  2. 20
      src/main/java/org/qortal/crosschain/ForeignBlockchainException.java

86
src/main/java/org/qortal/crosschain/ElectrumX.java

@ -7,6 +7,7 @@ import java.net.SocketAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -43,6 +44,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
// "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})" // "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})"
private static final Pattern DAEMON_ERROR_REGEX = Pattern.compile("DaemonError\\(\\{.*'code': ?(-?[0-9]+).*\\}\\)\\z"); // Capture 'code' inside curly-brace content private static final Pattern DAEMON_ERROR_REGEX = Pattern.compile("DaemonError\\(\\{.*'code': ?(-?[0-9]+).*\\}\\)\\z"); // Capture 'code' inside curly-brace content
/** Error message sent by some ElectrumX servers when they don't support returning verbose transactions. */
private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported";
public static class Server { public static class Server {
String hostname; String hostname;
@ -84,11 +88,13 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
} }
private Set<Server> servers = new HashSet<>(); private Set<Server> servers = new HashSet<>();
private List<Server> remainingServers = new ArrayList<>(); private List<Server> remainingServers = new ArrayList<>();
private Set<Server> uselessServers = Collections.synchronizedSet(new HashSet<>());
private final String netId; private final String netId;
private final String expectedGenesisHash; private final String expectedGenesisHash;
private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class); private final Map<Server.ConnectionType, Integer> defaultPorts = new EnumMap<>(Server.ConnectionType.class);
private final Object serverLock = new Object();
private Server currentServer; private Server currentServer;
private Socket socket; private Socket socket;
private Scanner scanner; private Scanner scanner;
@ -233,7 +239,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
public byte[] getRawTransaction(byte[] txHash) throws ForeignBlockchainException { public byte[] getRawTransaction(byte[] txHash) throws ForeignBlockchainException {
Object rawTransactionHex; Object rawTransactionHex;
try { try {
rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString()); rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString(), false);
} catch (ForeignBlockchainException.NetworkException e) { } catch (ForeignBlockchainException.NetworkException e) {
// DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'}) // DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})
if (Integer.valueOf(-5).equals(e.getDaemonErrorCode())) if (Integer.valueOf(-5).equals(e.getDaemonErrorCode()))
@ -256,7 +262,9 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
*/ */
@Override @Override
public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException { public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException {
Object transactionObj; Object transactionObj = null;
do {
try { try {
transactionObj = this.rpc("blockchain.transaction.get", txHash, true); transactionObj = this.rpc("blockchain.transaction.get", txHash, true);
} catch (ForeignBlockchainException.NetworkException e) { } catch (ForeignBlockchainException.NetworkException e) {
@ -266,10 +274,18 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
// Some servers also return non-standard responses like this: // Some servers also return non-standard responses like this:
// {"error":"verbose transactions are currently unsupported","id":3,"jsonrpc":"2.0"} // {"error":"verbose transactions are currently unsupported","id":3,"jsonrpc":"2.0"}
// We should probably try another server for these cases // We should probably not use this server any more
if (e.getServer() != null && VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE.equals(e.getMessage())) {
Server uselessServer = (Server) e.getServer();
LOGGER.trace(() -> String.format("Server %s doesn't support verbose transactions - barring use of that server", uselessServer));
this.uselessServers.add(uselessServer);
this.closeServer(uselessServer);
continue;
}
throw e; throw e;
} }
} while (transactionObj == null);
if (!(transactionObj instanceof JSONObject)) if (!(transactionObj instanceof JSONObject))
throw new ForeignBlockchainException.NetworkException("Expected JSONObject as response from ElectrumX blockchain.transaction.get RPC"); throw new ForeignBlockchainException.NetworkException("Expected JSONObject as response from ElectrumX blockchain.transaction.get RPC");
@ -441,7 +457,8 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
* @return "result" object from within JSON output * @return "result" object from within JSON output
* @throws ForeignBlockchainException if server returns error or something goes wrong * @throws ForeignBlockchainException if server returns error or something goes wrong
*/ */
private synchronized Object rpc(String method, Object...params) throws ForeignBlockchainException { private Object rpc(String method, Object...params) throws ForeignBlockchainException {
synchronized (this.serverLock) {
if (this.remainingServers.isEmpty()) if (this.remainingServers.isEmpty())
this.remainingServers.addAll(this.servers); this.remainingServers.addAll(this.servers);
@ -450,18 +467,14 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
if (response != null) if (response != null)
return response; return response;
this.currentServer = null; // Didn't work, try another server...
try { this.closeServer();
this.socket.close();
} catch (IOException e) {
/* ignore */
}
this.scanner = null;
} }
// Failed to perform RPC - maybe lack of servers? // Failed to perform RPC - maybe lack of servers?
throw new ForeignBlockchainException.NetworkException(String.format("Failed to perform ElectrumX RPC %s", method)); throw new ForeignBlockchainException.NetworkException(String.format("Failed to perform ElectrumX RPC %s", method));
} }
}
/** Returns true if we have, or create, a connection to an ElectrumX server. */ /** Returns true if we have, or create, a connection to an ElectrumX server. */
private boolean haveConnection() throws ForeignBlockchainException { private boolean haveConnection() throws ForeignBlockchainException {
@ -509,16 +522,8 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
this.currentServer = server; this.currentServer = server;
return true; return true;
} catch (IOException | ForeignBlockchainException | ClassCastException | NullPointerException e) { } catch (IOException | ForeignBlockchainException | ClassCastException | NullPointerException e) {
// Try another server... // Didn't work, try another server...
if (this.socket != null && !this.socket.isClosed()) closeServer();
try {
this.socket.close();
} catch (IOException e1) {
// We did try...
}
this.socket = null;
this.scanner = null;
} }
} }
@ -573,17 +578,17 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
Object errorObj = responseJson.get("error"); Object errorObj = responseJson.get("error");
if (errorObj != null) { if (errorObj != null) {
if (errorObj instanceof String) if (errorObj instanceof String)
throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error message from ElectrumX RPC %s: %s", method, (String) errorObj)); throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error message from ElectrumX RPC %s: %s", method, (String) errorObj), this.currentServer);
if (!(errorObj instanceof JSONObject)) if (!(errorObj instanceof JSONObject))
throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error response from ElectrumX RPC %s", method)); throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error response from ElectrumX RPC %s", method), this.currentServer);
JSONObject errorJson = (JSONObject) errorObj; JSONObject errorJson = (JSONObject) errorObj;
Object messageObj = errorJson.get("message"); Object messageObj = errorJson.get("message");
if (!(messageObj instanceof String)) if (!(messageObj instanceof String))
throw new ForeignBlockchainException.NetworkException(String.format("Missing/invalid message in error response from ElectrumX RPC %s", method)); throw new ForeignBlockchainException.NetworkException(String.format("Missing/invalid message in error response from ElectrumX RPC %s", method), this.currentServer);
String message = (String) messageObj; String message = (String) messageObj;
@ -594,15 +599,44 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
if (messageMatcher.find()) if (messageMatcher.find())
try { try {
int daemonErrorCode = Integer.parseInt(messageMatcher.group(1)); int daemonErrorCode = Integer.parseInt(messageMatcher.group(1));
throw new ForeignBlockchainException.NetworkException(daemonErrorCode, message); throw new ForeignBlockchainException.NetworkException(daemonErrorCode, message, this.currentServer);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
// We couldn't parse the error code integer? Fall-through to generic exception... // We couldn't parse the error code integer? Fall-through to generic exception...
} }
throw new ForeignBlockchainException.NetworkException(message); throw new ForeignBlockchainException.NetworkException(message, this.currentServer);
} }
return responseJson.get("result"); return responseJson.get("result");
} }
/**
* Closes connection to <tt>server</tt> if it is currently connected server.
* @param server
*/
private void closeServer(Server server) {
synchronized (this.serverLock) {
if (this.currentServer == null || !this.currentServer.equals(server))
return;
if (this.socket != null && !this.socket.isClosed())
try {
this.socket.close();
} catch (IOException e) {
// We did try...
}
this.socket = null;
this.scanner = null;
this.currentServer = null;
}
}
/** Closes connection to currently connected server (if any). */
private void closeServer() {
synchronized (this.serverLock) {
this.closeServer(this.currentServer);
}
}
} }

20
src/main/java/org/qortal/crosschain/ForeignBlockchainException.java

@ -13,25 +13,45 @@ public class ForeignBlockchainException extends Exception {
public static class NetworkException extends ForeignBlockchainException { public static class NetworkException extends ForeignBlockchainException {
private final Integer daemonErrorCode; private final Integer daemonErrorCode;
private final transient Object server;
public NetworkException() { public NetworkException() {
super(); super();
this.daemonErrorCode = null; this.daemonErrorCode = null;
this.server = null;
} }
public NetworkException(String message) { public NetworkException(String message) {
super(message); super(message);
this.daemonErrorCode = null; this.daemonErrorCode = null;
this.server = null;
} }
public NetworkException(int errorCode, String message) { public NetworkException(int errorCode, String message) {
super(message); super(message);
this.daemonErrorCode = errorCode; this.daemonErrorCode = errorCode;
this.server = null;
}
public NetworkException(String message, Object server) {
super(message);
this.daemonErrorCode = null;
this.server = server;
}
public NetworkException(int errorCode, String message, Object server) {
super(message);
this.daemonErrorCode = errorCode;
this.server = server;
} }
public Integer getDaemonErrorCode() { public Integer getDaemonErrorCode() {
return this.daemonErrorCode; return this.daemonErrorCode;
} }
public Object getServer() {
return this.server;
}
} }
public static class NotFoundException extends ForeignBlockchainException { public static class NotFoundException extends ForeignBlockchainException {

Loading…
Cancel
Save