forked from Qortal/qortal
Add support to ElectrumX for barring servers that don't give us the data we need
This commit is contained in:
parent
992427f0e0
commit
8707f154ee
@ -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,20 +262,30 @@ 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;
|
||||||
try {
|
|
||||||
transactionObj = this.rpc("blockchain.transaction.get", txHash, true);
|
|
||||||
} catch (ForeignBlockchainException.NetworkException e) {
|
|
||||||
// DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})
|
|
||||||
if (Integer.valueOf(-5).equals(e.getDaemonErrorCode()))
|
|
||||||
throw new ForeignBlockchainException.NotFoundException(e.getMessage());
|
|
||||||
|
|
||||||
// Some servers also return non-standard responses like this:
|
do {
|
||||||
// {"error":"verbose transactions are currently unsupported","id":3,"jsonrpc":"2.0"}
|
try {
|
||||||
// We should probably try another server for these cases
|
transactionObj = this.rpc("blockchain.transaction.get", txHash, true);
|
||||||
|
} catch (ForeignBlockchainException.NetworkException e) {
|
||||||
|
// DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})
|
||||||
|
if (Integer.valueOf(-5).equals(e.getDaemonErrorCode()))
|
||||||
|
throw new ForeignBlockchainException.NotFoundException(e.getMessage());
|
||||||
|
|
||||||
throw e;
|
// Some servers also return non-standard responses like this:
|
||||||
}
|
// {"error":"verbose transactions are currently unsupported","id":3,"jsonrpc":"2.0"}
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
} 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,26 +457,23 @@ 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 {
|
||||||
if (this.remainingServers.isEmpty())
|
synchronized (this.serverLock) {
|
||||||
this.remainingServers.addAll(this.servers);
|
if (this.remainingServers.isEmpty())
|
||||||
|
this.remainingServers.addAll(this.servers);
|
||||||
|
|
||||||
while (haveConnection()) {
|
while (haveConnection()) {
|
||||||
Object response = connectedRpc(method, params);
|
Object response = connectedRpc(method, params);
|
||||||
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. */
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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…
Reference in New Issue
Block a user