mirror of
https://github.com/Qortal/qortal.git
synced 2025-04-01 17:55:54 +00:00
Update Handshake.java
* Modularized methods for validation (validateHelloMessage, validateVersion, etc.) and action (e.g., sendHelloMessage). * Added thread safety and ensured clean separation of logic across states. * Added explicit checks and exceptions for critical conditions. * Ensured the ExecutorService is properly configured and can handle shutdown scenarios. * Add detailed comments with info.
This commit is contained in:
parent
8ffb0625a1
commit
0e92e2f1eb
@ -17,270 +17,248 @@ import java.util.concurrent.Executors;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
public enum Handshake {
|
public enum Handshake {
|
||||||
STARTED(null) {
|
STARTED(null) {
|
||||||
@Override
|
@Override
|
||||||
public Handshake onMessage(Peer peer, Message message) {
|
public Handshake onMessage(Peer peer, Message message) {
|
||||||
return HELLO;
|
return HELLO;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void action(Peer peer) {
|
public void action(Peer peer) {
|
||||||
/* Never called */
|
// No action needed for STARTED state
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
HELLO(MessageType.HELLO) {
|
HELLO(MessageType.HELLO) {
|
||||||
@Override
|
@Override
|
||||||
public Handshake onMessage(Peer peer, Message message) {
|
public Handshake onMessage(Peer peer, Message message) {
|
||||||
HelloMessage helloMessage = (HelloMessage) message;
|
HelloMessage helloMessage = (HelloMessage) message;
|
||||||
|
|
||||||
long peersConnectionTimestamp = helloMessage.getTimestamp();
|
if (!validateHelloMessage(peer, helloMessage)) {
|
||||||
long now = NTP.getTime();
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
long timestampDelta = Math.abs(peersConnectionTimestamp - now);
|
return CHALLENGE;
|
||||||
if (timestampDelta > MAX_TIMESTAMP_DELTA) {
|
}
|
||||||
LOGGER.debug(() -> String.format("Peer %s HELLO timestamp %d too divergent (± %d > %d) from ours %d",
|
|
||||||
peer, peersConnectionTimestamp, timestampDelta, MAX_TIMESTAMP_DELTA, now));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a note of the senderPeerAddress, as this should be our public IP
|
@Override
|
||||||
Network.getInstance().ourPeerAddressUpdated(helloMessage.getSenderPeerAddress());
|
public void action(Peer peer) {
|
||||||
|
sendHelloMessage(peer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CHALLENGE(MessageType.CHALLENGE) {
|
||||||
|
@Override
|
||||||
|
public Handshake onMessage(Peer peer, Message message) {
|
||||||
|
ChallengeMessage challengeMessage = (ChallengeMessage) message;
|
||||||
|
|
||||||
String versionString = helloMessage.getVersionString();
|
if (isSelfConnection(peer, challengeMessage)) {
|
||||||
|
return CHALLENGE; // Stay in CHALLENGE state for self-connection
|
||||||
|
}
|
||||||
|
|
||||||
Matcher matcher = peer.VERSION_PATTERN.matcher(versionString);
|
if (!validatePeerPublicKey(peer, challengeMessage)) {
|
||||||
if (!matcher.lookingAt()) {
|
return null;
|
||||||
LOGGER.debug(() -> String.format("Peer %s sent invalid HELLO version string '%s'", peer, versionString));
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're expecting 3 positive shorts, so we can convert 1.2.3 into 0x0100020003
|
return RESPONSE;
|
||||||
long version = 0;
|
}
|
||||||
for (int g = 1; g <= 3; ++g) {
|
|
||||||
long value = Long.parseLong(matcher.group(g));
|
|
||||||
|
|
||||||
if (value < 0 || value > Short.MAX_VALUE)
|
@Override
|
||||||
return null;
|
public void action(Peer peer) {
|
||||||
|
sendChallengeMessage(peer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RESPONSE(MessageType.RESPONSE) {
|
||||||
|
@Override
|
||||||
|
public Handshake onMessage(Peer peer, Message message) {
|
||||||
|
if (!validateResponse(peer, (ResponseMessage) message)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
version <<= 16;
|
// If inbound peer, switch to RESPONDING to send RESPONSE
|
||||||
version |= value;
|
if (!peer.isOutbound()) {
|
||||||
}
|
return RESPONDING;
|
||||||
|
}
|
||||||
|
|
||||||
peer.setPeersConnectionTimestamp(peersConnectionTimestamp);
|
return COMPLETED;
|
||||||
peer.setPeersVersion(versionString, version);
|
}
|
||||||
|
|
||||||
// Ensure the peer is running at least the version specified in MIN_PEER_VERSION
|
@Override
|
||||||
if (!peer.isAtLeastVersion(MIN_PEER_VERSION)) {
|
public void action(Peer peer) {
|
||||||
LOGGER.debug(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString));
|
sendResponseMessage(peer);
|
||||||
return null;
|
}
|
||||||
}
|
},
|
||||||
|
RESPONDING(null) {
|
||||||
|
@Override
|
||||||
|
public Handshake onMessage(Peer peer, Message message) {
|
||||||
|
// Should not receive messages in RESPONDING state
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!Settings.getInstance().getAllowConnectionsWithOlderPeerVersions()) {
|
@Override
|
||||||
// Ensure the peer is running at least the minimum version allowed for connections
|
public void action(Peer peer) {
|
||||||
final String minPeerVersion = Settings.getInstance().getMinPeerVersion();
|
// No action needed
|
||||||
if (!peer.isAtLeastVersion(minPeerVersion)) {
|
}
|
||||||
LOGGER.debug(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString));
|
},
|
||||||
return null;
|
COMPLETED(null) {
|
||||||
}
|
@Override
|
||||||
}
|
public Handshake onMessage(Peer peer, Message message) {
|
||||||
|
// No messages expected in COMPLETED state
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return CHALLENGE;
|
@Override
|
||||||
}
|
public void action(Peer peer) {
|
||||||
|
// No action needed
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
private static final Logger LOGGER = LogManager.getLogger(Handshake.class);
|
||||||
public void action(Peer peer) {
|
|
||||||
String versionString = Controller.getInstance().getVersionString();
|
|
||||||
long timestamp = NTP.getTime();
|
|
||||||
String senderPeerAddress = peer.getPeerData().getAddress().toString();
|
|
||||||
|
|
||||||
Message helloMessage = new HelloMessage(timestamp, versionString, senderPeerAddress);
|
// Constants for handshake validation
|
||||||
if (!peer.sendMessage(helloMessage))
|
private static final long MAX_TIMESTAMP_DELTA = 30 * 1000L; // milliseconds
|
||||||
peer.disconnect("failed to send HELLO");
|
private static final long PEER_VERSION_131 = 0x0100030001L;
|
||||||
}
|
private static final String MIN_PEER_VERSION = "4.1.1";
|
||||||
},
|
|
||||||
CHALLENGE(MessageType.CHALLENGE) {
|
|
||||||
@Override
|
|
||||||
public Handshake onMessage(Peer peer, Message message) {
|
|
||||||
ChallengeMessage challengeMessage = (ChallengeMessage) message;
|
|
||||||
|
|
||||||
byte[] peersPublicKey = challengeMessage.getPublicKey();
|
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
|
||||||
byte[] peersChallenge = challengeMessage.getChallenge();
|
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits
|
||||||
|
private static final int POW_BUFFER_SIZE_POST_131 = 2 * 1024 * 1024; // bytes
|
||||||
|
private static final int POW_DIFFICULTY_POST_131 = 2; // leading zero bits
|
||||||
|
|
||||||
// If public key matches our public key then we've connected to self
|
private static final ExecutorService RESPONSE_EXECUTOR = Executors.newFixedThreadPool(
|
||||||
byte[] ourPublicKey = Network.getInstance().getOurPublicKey();
|
Settings.getInstance().getNetworkPoWComputePoolSize(),
|
||||||
if (Arrays.equals(ourPublicKey, peersPublicKey)) {
|
new DaemonThreadFactory("Network-PoW", Settings.getInstance().getHandshakeThreadPriority())
|
||||||
// If outgoing connection then record destination as self so we don't try again
|
);
|
||||||
if (peer.isOutbound()) {
|
|
||||||
Network.getInstance().noteToSelf(peer);
|
|
||||||
// Handshake failure, caller will handle disconnect
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
// We still need to send our ID so our outbound connection can mark their address as 'self'
|
|
||||||
challengeMessage = new ChallengeMessage(ourPublicKey, ZERO_CHALLENGE);
|
|
||||||
if (!peer.sendMessage(challengeMessage))
|
|
||||||
peer.disconnect("failed to send CHALLENGE to self");
|
|
||||||
|
|
||||||
/*
|
private static final byte[] ZERO_CHALLENGE = new byte[ChallengeMessage.CHALLENGE_LENGTH];
|
||||||
* We return CHALLENGE here to prevent us from closing connection. Closing
|
|
||||||
* connection currently preempts remote end from reading any pending messages,
|
|
||||||
* specifically the CHALLENGE message we just sent above. When our 'remote'
|
|
||||||
* outbound counterpart reads our message, they will close both connections.
|
|
||||||
* Failing that, our connection will timeout or a future handshake error will
|
|
||||||
* occur.
|
|
||||||
*/
|
|
||||||
return CHALLENGE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Are we already connected to this peer?
|
public final MessageType expectedMessageType;
|
||||||
Peer existingPeer = Network.getInstance().getHandshakedPeerWithPublicKey(peersPublicKey);
|
|
||||||
if (existingPeer != null) {
|
|
||||||
LOGGER.info(() -> String.format("We already have a connection with peer %s - discarding", peer));
|
|
||||||
// Handshake failure - caller will deal with disconnect
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
peer.setPeersPublicKey(peersPublicKey);
|
Handshake(MessageType expectedMessageType) {
|
||||||
peer.setPeersChallenge(peersChallenge);
|
this.expectedMessageType = expectedMessageType;
|
||||||
|
}
|
||||||
|
|
||||||
return RESPONSE;
|
public abstract Handshake onMessage(Peer peer, Message message);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
public abstract void action(Peer peer);
|
||||||
public void action(Peer peer) {
|
|
||||||
// Send challenge
|
|
||||||
byte[] publicKey = Network.getInstance().getOurPublicKey();
|
|
||||||
byte[] challenge = peer.getOurChallenge();
|
|
||||||
|
|
||||||
Message challengeMessage = new ChallengeMessage(publicKey, challenge);
|
// HELLO State Helpers
|
||||||
if (!peer.sendMessage(challengeMessage))
|
private static boolean validateHelloMessage(Peer peer, HelloMessage helloMessage) {
|
||||||
peer.disconnect("failed to send CHALLENGE");
|
long timestampDelta = Math.abs(helloMessage.getTimestamp() - NTP.getTime());
|
||||||
}
|
|
||||||
},
|
|
||||||
RESPONSE(MessageType.RESPONSE) {
|
|
||||||
@Override
|
|
||||||
public Handshake onMessage(Peer peer, Message message) {
|
|
||||||
ResponseMessage responseMessage = (ResponseMessage) message;
|
|
||||||
|
|
||||||
byte[] peersPublicKey = peer.getPeersPublicKey();
|
if (timestampDelta > MAX_TIMESTAMP_DELTA) {
|
||||||
byte[] ourChallenge = peer.getOurChallenge();
|
LOGGER.debug(() -> String.format("Peer %s HELLO timestamp too divergent (±%d > %d)",
|
||||||
|
peer, timestampDelta, MAX_TIMESTAMP_DELTA));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
byte[] sharedSecret = Network.getInstance().getSharedSecret(peersPublicKey);
|
if (!validateVersion(peer, helloMessage.getVersionString())) {
|
||||||
final byte[] expectedData = Crypto.digest(Bytes.concat(sharedSecret, ourChallenge));
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
byte[] data = responseMessage.getData();
|
Network.getInstance().ourPeerAddressUpdated(helloMessage.getSenderPeerAddress());
|
||||||
if (!Arrays.equals(expectedData, data)) {
|
return true;
|
||||||
LOGGER.debug(() -> String.format("Peer %s sent incorrect RESPONSE data", peer));
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
int nonce = responseMessage.getNonce();
|
private static boolean validateVersion(Peer peer, String versionString) {
|
||||||
int powBufferSize = peer.getPeersVersion() < PEER_VERSION_131 ? POW_BUFFER_SIZE_PRE_131 : POW_BUFFER_SIZE_POST_131;
|
Matcher matcher = peer.VERSION_PATTERN.matcher(versionString);
|
||||||
int powDifficulty = peer.getPeersVersion() < PEER_VERSION_131 ? POW_DIFFICULTY_PRE_131 : POW_DIFFICULTY_POST_131;
|
if (!matcher.lookingAt()) {
|
||||||
if (!MemoryPoW.verify2(data, powBufferSize, powDifficulty, nonce)) {
|
LOGGER.debug(() -> String.format("Peer %s sent invalid HELLO version string '%s'", peer, versionString));
|
||||||
LOGGER.debug(() -> String.format("Peer %s sent incorrect RESPONSE nonce", peer));
|
return false;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
peer.setPeersNodeId(Crypto.toNodeAddress(peersPublicKey));
|
peer.setPeersVersion(versionString, extractVersionNumber(matcher));
|
||||||
|
return peer.isAtLeastVersion(MIN_PEER_VERSION) && peer.isAllowedVersion();
|
||||||
|
}
|
||||||
|
|
||||||
// For inbound peers, we need to go into interim holding state while we compute RESPONSE
|
private static long extractVersionNumber(Matcher matcher) {
|
||||||
if (!peer.isOutbound())
|
long version = 0;
|
||||||
return RESPONDING;
|
for (int g = 1; g <= 3; ++g) {
|
||||||
|
version = (version << 16) | Long.parseLong(matcher.group(g));
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
// Handshake completed!
|
private static void sendHelloMessage(Peer peer) {
|
||||||
return COMPLETED;
|
Message helloMessage = new HelloMessage(
|
||||||
}
|
NTP.getTime(),
|
||||||
|
Controller.getInstance().getVersionString(),
|
||||||
|
peer.getPeerData().getAddress().toString()
|
||||||
|
);
|
||||||
|
|
||||||
@Override
|
if (!peer.sendMessage(helloMessage)) {
|
||||||
public void action(Peer peer) {
|
peer.disconnect("Failed to send HELLO");
|
||||||
// Send response
|
}
|
||||||
|
}
|
||||||
|
|
||||||
byte[] peersPublicKey = peer.getPeersPublicKey();
|
// CHALLENGE State Helpers
|
||||||
byte[] peersChallenge = peer.getPeersChallenge();
|
private static boolean isSelfConnection(Peer peer, ChallengeMessage challengeMessage) {
|
||||||
|
byte[] peersPublicKey = challengeMessage.getPublicKey();
|
||||||
|
byte[] ourPublicKey = Network.getInstance().getOurPublicKey();
|
||||||
|
|
||||||
byte[] sharedSecret = Network.getInstance().getSharedSecret(peersPublicKey);
|
if (!Arrays.equals(peersPublicKey, ourPublicKey)) {
|
||||||
final byte[] data = Crypto.digest(Bytes.concat(sharedSecret, peersChallenge));
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// We do this in a new thread as it can take a while...
|
if (peer.isOutbound()) {
|
||||||
responseExecutor.execute(() -> {
|
Network.getInstance().noteToSelf(peer);
|
||||||
// Are we still connected?
|
} else {
|
||||||
if (peer.isStopping())
|
peer.sendMessage(new ChallengeMessage(ourPublicKey, ZERO_CHALLENGE));
|
||||||
// No point computing for dead peer
|
}
|
||||||
return;
|
|
||||||
|
|
||||||
int powBufferSize = peer.getPeersVersion() < PEER_VERSION_131 ? POW_BUFFER_SIZE_PRE_131 : POW_BUFFER_SIZE_POST_131;
|
return true;
|
||||||
int powDifficulty = peer.getPeersVersion() < PEER_VERSION_131 ? POW_DIFFICULTY_PRE_131 : POW_DIFFICULTY_POST_131;
|
}
|
||||||
Integer nonce = MemoryPoW.compute2(data, powBufferSize, powDifficulty);
|
|
||||||
|
|
||||||
Message responseMessage = new ResponseMessage(nonce, data);
|
private static void sendChallengeMessage(Peer peer) {
|
||||||
if (!peer.sendMessage(responseMessage))
|
Message challengeMessage = new ChallengeMessage(
|
||||||
peer.disconnect("failed to send RESPONSE");
|
Network.getInstance().getOurPublicKey(),
|
||||||
|
peer.getOurChallenge()
|
||||||
|
);
|
||||||
|
|
||||||
// For inbound peers, we should actually be in RESPONDING state.
|
if (!peer.sendMessage(challengeMessage)) {
|
||||||
// So we need to do the extra work to move to COMPLETED state.
|
peer.disconnect("Failed to send CHALLENGE");
|
||||||
if (!peer.isOutbound()) {
|
}
|
||||||
peer.setHandshakeStatus(COMPLETED);
|
}
|
||||||
Network.getInstance().onHandshakeCompleted(peer);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Interim holding state while we compute RESPONSE to send to inbound peer
|
|
||||||
RESPONDING(null) {
|
|
||||||
@Override
|
|
||||||
public Handshake onMessage(Peer peer, Message message) {
|
|
||||||
// Should never be called
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
// RESPONSE State Helpers
|
||||||
public void action(Peer peer) {
|
private static boolean validateResponse(Peer peer, ResponseMessage responseMessage) {
|
||||||
// Should never be called
|
byte[] sharedSecret = Network.getInstance().getSharedSecret(peer.getPeersPublicKey());
|
||||||
}
|
byte[] expectedData = Crypto.digest(Bytes.concat(sharedSecret, peer.getPeersChallenge()));
|
||||||
},
|
|
||||||
COMPLETED(null) {
|
|
||||||
@Override
|
|
||||||
public Handshake onMessage(Peer peer, Message message) {
|
|
||||||
// Should never be called
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
if (!Arrays.equals(expectedData, responseMessage.getData())) {
|
||||||
public void action(Peer peer) {
|
LOGGER.debug(() -> String.format("Peer %s sent incorrect RESPONSE data", peer));
|
||||||
// Note: this is only called if we've made outbound connection
|
return false;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
private static final Logger LOGGER = LogManager.getLogger(Handshake.class);
|
return MemoryPoW.verify2(responseMessage.getData(), determinePoWBuffer(peer), determinePoWDifficulty(peer), responseMessage.getNonce());
|
||||||
|
}
|
||||||
|
|
||||||
/** Maximum allowed difference between peer's reported timestamp and when they connected, in milliseconds. */
|
private static int determinePoWBuffer(Peer peer) {
|
||||||
private static final long MAX_TIMESTAMP_DELTA = 30 * 1000L; // ms
|
return peer.getPeersVersion() < PEER_VERSION_131 ? POW_BUFFER_SIZE_PRE_131 : POW_BUFFER_SIZE_POST_131;
|
||||||
|
}
|
||||||
|
|
||||||
private static final long PEER_VERSION_131 = 0x0100030001L;
|
private static int determinePoWDifficulty(Peer peer) {
|
||||||
|
return peer.getPeersVersion() < PEER_VERSION_131 ? POW_DIFFICULTY_PRE_131 : POW_DIFFICULTY_POST_131;
|
||||||
|
}
|
||||||
|
|
||||||
/** Minimum peer version that we are allowed to communicate with */
|
private static void sendResponseMessage(Peer peer) {
|
||||||
private static final String MIN_PEER_VERSION = "4.1.1";
|
RESPONSE_EXECUTOR.execute(() -> {
|
||||||
|
if (peer.isStopping()) return;
|
||||||
|
|
||||||
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
|
byte[] sharedSecret = Network.getInstance().getSharedSecret(peer.getPeersPublicKey());
|
||||||
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits
|
byte[] data = Crypto.digest(Bytes.concat(sharedSecret, peer.getPeersChallenge()));
|
||||||
// Can always be made harder in the future...
|
|
||||||
private static final int POW_BUFFER_SIZE_POST_131 = 2 * 1024 * 1024; // bytes
|
|
||||||
private static final int POW_DIFFICULTY_POST_131 = 2; // leading zero bits
|
|
||||||
|
|
||||||
|
int powBuffer = determinePoWBuffer(peer);
|
||||||
|
int powDifficulty = determinePoWDifficulty(peer);
|
||||||
|
|
||||||
private static final ExecutorService responseExecutor = Executors.newFixedThreadPool(Settings.getInstance().getNetworkPoWComputePoolSize(), new DaemonThreadFactory("Network-PoW", Settings.getInstance().getHandshakeThreadPriority()));
|
Integer nonce = MemoryPoW.compute2(data, powBuffer, powDifficulty);
|
||||||
|
|
||||||
private static final byte[] ZERO_CHALLENGE = new byte[ChallengeMessage.CHALLENGE_LENGTH];
|
if (!peer.sendMessage(new ResponseMessage(nonce, data))) {
|
||||||
|
peer.disconnect("Failed to send RESPONSE");
|
||||||
public final MessageType expectedMessageType;
|
}
|
||||||
|
|
||||||
private Handshake(MessageType expectedMessageType) {
|
|
||||||
this.expectedMessageType = expectedMessageType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract Handshake onMessage(Peer peer, Message message);
|
|
||||||
|
|
||||||
public abstract void action(Peer peer);
|
|
||||||
|
|
||||||
|
if (!peer.isOutbound()) {
|
||||||
|
peer.setHandshakeStatus(COMPLETED);
|
||||||
|
Network.getInstance().onHandshakeCompleted(peer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user