Merge 0e92e2f1ebfe49f0c9269dd252387b38b0f892f0 into 8ffb0625a1edcf0b3d1ec2498b15a31ec38ade3c

This commit is contained in:
cwd.systems | 0KN 2024-11-27 11:21:48 +00:00 committed by GitHub
commit 3eea787c25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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);
}
});
}
} }