From 0c32afa07f92918002568a035fdd6a389da8e635 Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 22 May 2020 17:16:45 +0100 Subject: [PATCH] New network handshaking. NOT backwards compatible! Old Qora v1 message types removed. Message type values changed. Network handshaking reworked to fix multiple-connections issue. Instead of using some random peerID, we now use proper keypairs and a challenge-response handshake to prevent doppelgangers/ID-theft. This results in simpler handshaking code as we don't have to perform some arcane doppelganger resolution. Handshaking still uses proof-of-work for challenge-response, but switched to newer MemoryPoW. API call GET /peers no longer has 'buildTimestamp' field, but does now have 'nodeId' field. Network no longer has a whole raft of getXXXpeers() due to simplified handshaking. Quite a few method calls changed to simply Network.getHandshakedPeers(), which is also faster. --- .../org/qortal/api/model/ConnectedPeer.java | 10 +- .../java/org/qortal/api/model/NodeInfo.java | 1 + .../qortal/api/resource/AdminResource.java | 1 + .../java/org/qortal/block/BlockMinter.java | 2 +- .../org/qortal/controller/Controller.java | 151 +-------- .../org/qortal/controller/Synchronizer.java | 11 +- .../java/org/qortal/network/Handshake.java | 309 ++++++++---------- src/main/java/org/qortal/network/Network.java | 234 ++++--------- src/main/java/org/qortal/network/Peer.java | 239 ++++++++------ src/main/java/org/qortal/network/Proof.java | 126 ------- .../network/message/ChallengeMessage.java | 56 ++++ .../network/message/GetSignaturesMessage.java | 54 --- .../network/message/GoodbyeMessage.java | 65 ++++ .../qortal/network/message/HeightMessage.java | 47 --- .../qortal/network/message/HelloMessage.java | 55 ++++ .../org/qortal/network/message/Message.java | 66 ++-- .../qortal/network/message/PeerIdMessage.java | 52 --- .../network/message/PeerVerifyMessage.java | 51 --- .../qortal/network/message/PeersMessage.java | 79 ----- .../qortal/network/message/ProofMessage.java | 63 ---- .../network/message/ResponseMessage.java | 55 ++++ .../message/VerificationCodesMessage.java | 64 ---- .../network/message/VersionMessage.java | 68 ---- 23 files changed, 622 insertions(+), 1237 deletions(-) delete mode 100644 src/main/java/org/qortal/network/Proof.java create mode 100644 src/main/java/org/qortal/network/message/ChallengeMessage.java delete mode 100644 src/main/java/org/qortal/network/message/GetSignaturesMessage.java create mode 100644 src/main/java/org/qortal/network/message/GoodbyeMessage.java delete mode 100644 src/main/java/org/qortal/network/message/HeightMessage.java create mode 100644 src/main/java/org/qortal/network/message/HelloMessage.java delete mode 100644 src/main/java/org/qortal/network/message/PeerIdMessage.java delete mode 100644 src/main/java/org/qortal/network/message/PeerVerifyMessage.java delete mode 100644 src/main/java/org/qortal/network/message/PeersMessage.java delete mode 100644 src/main/java/org/qortal/network/message/ProofMessage.java create mode 100644 src/main/java/org/qortal/network/message/ResponseMessage.java delete mode 100644 src/main/java/org/qortal/network/message/VerificationCodesMessage.java delete mode 100644 src/main/java/org/qortal/network/message/VersionMessage.java diff --git a/src/main/java/org/qortal/api/model/ConnectedPeer.java b/src/main/java/org/qortal/api/model/ConnectedPeer.java index 0926f9a4..3209ee6a 100644 --- a/src/main/java/org/qortal/api/model/ConnectedPeer.java +++ b/src/main/java/org/qortal/api/model/ConnectedPeer.java @@ -25,7 +25,8 @@ public class ConnectedPeer { public String address; public String version; - public Long buildTimestamp; + + public String nodeId; public Integer lastHeight; @Schema(example = "base58") @@ -45,10 +46,9 @@ public class ConnectedPeer { this.peersConnectedWhen = peer.getPeersConnectionTimestamp(); this.address = peerData.getAddress().toString(); - if (peer.getVersionMessage() != null) { - this.version = peer.getVersionMessage().getVersionString(); - this.buildTimestamp = peer.getVersionMessage().getBuildTimestamp(); - } + + this.version = peer.getPeersVersionString(); + this.nodeId = peer.getPeersNodeId(); PeerChainTipData peerChainTipData = peer.getChainTipData(); if (peerChainTipData != null) { diff --git a/src/main/java/org/qortal/api/model/NodeInfo.java b/src/main/java/org/qortal/api/model/NodeInfo.java index 51cf2ae3..86ed6971 100644 --- a/src/main/java/org/qortal/api/model/NodeInfo.java +++ b/src/main/java/org/qortal/api/model/NodeInfo.java @@ -10,6 +10,7 @@ public class NodeInfo { public long uptime; public String buildVersion; public long buildTimestamp; + public String nodeId; public NodeInfo() { } diff --git a/src/main/java/org/qortal/api/resource/AdminResource.java b/src/main/java/org/qortal/api/resource/AdminResource.java index 18650793..db4d3026 100644 --- a/src/main/java/org/qortal/api/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/resource/AdminResource.java @@ -117,6 +117,7 @@ public class AdminResource { nodeInfo.uptime = System.currentTimeMillis() - Controller.startTime; nodeInfo.buildVersion = Controller.getInstance().getVersionString(); nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp(); + nodeInfo.nodeId = Network.getInstance().getOurNodeId(); return nodeInfo; } diff --git a/src/main/java/org/qortal/block/BlockMinter.java b/src/main/java/org/qortal/block/BlockMinter.java index f5d2ea7f..bffee187 100644 --- a/src/main/java/org/qortal/block/BlockMinter.java +++ b/src/main/java/org/qortal/block/BlockMinter.java @@ -128,7 +128,7 @@ public class BlockMinter extends Thread { } } - List peers = Network.getInstance().getUniqueHandshakedPeers(); + List peers = Network.getInstance().getHandshakedPeers(); BlockData lastBlockData = blockRepository.getLastBlock(); // Disregard peers that have "misbehaved" recently diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index b96d3bef..957b1d04 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -60,11 +60,9 @@ import org.qortal.network.message.GetBlockMessage; import org.qortal.network.message.GetBlockSummariesMessage; import org.qortal.network.message.GetOnlineAccountsMessage; import org.qortal.network.message.GetPeersMessage; -import org.qortal.network.message.GetSignaturesMessage; import org.qortal.network.message.GetSignaturesV2Message; import org.qortal.network.message.GetTransactionMessage; import org.qortal.network.message.GetUnconfirmedTransactionsMessage; -import org.qortal.network.message.HeightMessage; import org.qortal.network.message.HeightV2Message; import org.qortal.network.message.Message; import org.qortal.network.message.OnlineAccountsMessage; @@ -501,7 +499,7 @@ public class Controller extends Thread { }; private void potentiallySynchronize() throws InterruptedException { - List peers = Network.getInstance().getUniqueHandshakedPeers(); + List peers = Network.getInstance().getHandshakedPeers(); // Disregard peers that have "misbehaved" recently peers.removeIf(hasMisbehaved); @@ -626,7 +624,7 @@ public class Controller extends Thread { return; } - final int numberOfPeers = Network.getInstance().getUniqueHandshakedPeers().size(); + final int numberOfPeers = Network.getInstance().getHandshakedPeers().size(); final int height = getChainHeight(); @@ -782,32 +780,11 @@ public class Controller extends Thread { public void onPeerHandshakeCompleted(Peer peer) { // Only send if outbound if (peer.isOutbound()) { - if (peer.getVersion() < 2) { - // Legacy mode - - // Send our unconfirmed transactions - try (final Repository repository = RepositoryManager.getRepository()) { - List transactions = repository.getTransactionRepository().getUnconfirmedTransactions(); - - for (TransactionData transactionData : transactions) { - Message transactionMessage = new TransactionMessage(transactionData); - if (!peer.sendMessage(transactionMessage)) { - peer.disconnect("failed to send unconfirmed transaction"); - return; - } - } - } catch (DataException e) { - LOGGER.error("Repository issue while sending unconfirmed transactions", e); - } - } else { - // V2 protocol - - // Request peer's unconfirmed transactions - Message message = new GetUnconfirmedTransactionsMessage(); - if (!peer.sendMessage(message)) { - peer.disconnect("failed to send request for unconfirmed transactions"); - return; - } + // Request peer's unconfirmed transactions + Message message = new GetUnconfirmedTransactionsMessage(); + if (!peer.sendMessage(message)) { + peer.disconnect("failed to send request for unconfirmed transactions"); + return; } } @@ -823,22 +800,10 @@ public class Controller extends Thread { // Ordered by message type value switch (message.getType()) { - case HEIGHT: - onNetworkHeightMessage(peer, message); - break; - - case GET_SIGNATURES: - onNetworkGetSignaturesMessage(peer, message); - break; - case GET_BLOCK: onNetworkGetBlockMessage(peer, message); break; - case BLOCK: - onNetworkBlockMessage(peer, message); - break; - case TRANSACTION: onNetworkTransactionMessage(peer, message); break; @@ -884,56 +849,11 @@ public class Controller extends Thread { break; default: - LOGGER.debug(String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer)); + LOGGER.debug(() -> String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer)); break; } } - private void onNetworkHeightMessage(Peer peer, Message message) { - HeightMessage heightMessage = (HeightMessage) message; - - // Update all peers with same ID - - List connectedPeers = Network.getInstance().getHandshakedPeers(); - for (Peer connectedPeer : connectedPeers) { - if (connectedPeer.getPeerId() == null || !Arrays.equals(connectedPeer.getPeerId(), peer.getPeerId())) - continue; - - // Update peer chain tip data - PeerChainTipData newChainTipData = new PeerChainTipData(heightMessage.getHeight(), null, null, null); - connectedPeer.setChainTipData(newChainTipData); - } - - // Potentially synchronize - requestSync = true; - } - - private void onNetworkGetSignaturesMessage(Peer peer, Message message) { - GetSignaturesMessage getSignaturesMessage = (GetSignaturesMessage) message; - byte[] parentSignature = getSignaturesMessage.getParentSignature(); - - try (final Repository repository = RepositoryManager.getRepository()) { - List signatures = new ArrayList<>(); - - do { - BlockData blockData = repository.getBlockRepository().fromReference(parentSignature); - - if (blockData == null) - break; - - parentSignature = blockData.getSignature(); - signatures.add(parentSignature); - } while (signatures.size() < Network.MAX_SIGNATURES_PER_REPLY); - - Message signaturesMessage = new SignaturesMessage(signatures); - signaturesMessage.setId(message.getId()); - if (!peer.sendMessage(signaturesMessage)) - peer.disconnect("failed to send signatures"); - } catch (DataException e) { - LOGGER.error(String.format("Repository issue while sending signatures after %s to peer %s", Base58.encode(parentSignature), peer), e); - } - } - private void onNetworkGetBlockMessage(Peer peer, Message message) { GetBlockMessage getBlockMessage = (GetBlockMessage) message; byte[] signature = getBlockMessage.getSignature(); @@ -957,40 +877,6 @@ public class Controller extends Thread { } } - private void onNetworkBlockMessage(Peer peer, Message message) { - // From a v1 peer, with no message ID, this is a broadcast of peer's latest block - // v2 peers announce new blocks using HEIGHT_V2 - - // Not version 1? - if (peer.getVersion() == null || peer.getVersion() > 1) - return; - - // Message ID present? - // XXX Why is this test here? If BLOCK had an ID then surely it would be a response to GET_BLOCK - // and hence captured by Peer's reply queue? - if (message.hasId()) - return; - - BlockMessage blockMessage = (BlockMessage) message; - BlockData blockData = blockMessage.getBlockData(); - - // Update all peers with same ID - - List connectedPeers = Network.getInstance().getHandshakedPeers(); - for (Peer connectedPeer : connectedPeers) { - // Skip connectedPeer if they have no ID or their ID doesn't match sender's ID - if (connectedPeer.getPeerId() == null || !Arrays.equals(connectedPeer.getPeerId(), peer.getPeerId())) - continue; - - // Update peer chain tip data - PeerChainTipData newChainTipData = new PeerChainTipData(blockData.getHeight(), blockData.getSignature(), blockData.getTimestamp(), blockData.getMinterPublicKey()); - connectedPeer.setChainTipData(newChainTipData); - } - - // Potentially synchronize - requestSync = true; - } - private void onNetworkTransactionMessage(Peer peer, Message message) { TransactionMessage transactionMessage = (TransactionMessage) message; TransactionData transactionData = transactionMessage.getTransactionData(); @@ -1096,18 +982,9 @@ public class Controller extends Thread { if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null)) peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip())); - // Update all peers with same ID - - List connectedPeers = Network.getInstance().getHandshakedPeers(); - for (Peer connectedPeer : connectedPeers) { - // Skip connectedPeer if they have no ID or their ID doesn't match sender's ID - if (connectedPeer.getPeerId() == null || !Arrays.equals(connectedPeer.getPeerId(), peer.getPeerId())) - continue; - - // Update peer chain tip data - PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getMinterPublicKey()); - connectedPeer.setChainTipData(newChainTipData); - } + // Update peer chain tip data + PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getMinterPublicKey()); + peer.setChainTipData(newChainTipData); // Potentially synchronize requestSync = true; @@ -1561,7 +1438,7 @@ public class Controller extends Thread { getArbitraryDataMessage.setId(id); // Broadcast request - Network.getInstance().broadcast(peer -> peer.getVersion() < 2 ? null : getArbitraryDataMessage); + Network.getInstance().broadcast(peer -> getArbitraryDataMessage); // Poll to see if data has arrived final long singleWait = 100; @@ -1593,7 +1470,7 @@ public class Controller extends Thread { if (minLatestBlockTimestamp == null) return null; - List peers = Network.getInstance().getUniqueHandshakedPeers(); + List peers = Network.getInstance().getHandshakedPeers(); // Filter out unsuitable peers Iterator iterator = peers.iterator(); @@ -1639,7 +1516,7 @@ public class Controller extends Thread { if (latestBlockData == null || latestBlockData.getTimestamp() < minLatestBlockTimestamp) return false; - List peers = Network.getInstance().getUniqueHandshakedPeers(); + List peers = Network.getInstance().getHandshakedPeers(); if (peers == null) return false; diff --git a/src/main/java/org/qortal/controller/Synchronizer.java b/src/main/java/org/qortal/controller/Synchronizer.java index 29087574..e7dc6dd2 100644 --- a/src/main/java/org/qortal/controller/Synchronizer.java +++ b/src/main/java/org/qortal/controller/Synchronizer.java @@ -22,7 +22,6 @@ import org.qortal.network.message.BlockMessage; import org.qortal.network.message.BlockSummariesMessage; import org.qortal.network.message.GetBlockMessage; import org.qortal.network.message.GetBlockSummariesMessage; -import org.qortal.network.message.GetSignaturesMessage; import org.qortal.network.message.GetSignaturesV2Message; import org.qortal.network.message.Message; import org.qortal.network.message.SignaturesMessage; @@ -372,12 +371,7 @@ public class Synchronizer { return SynchronizationResult.TOO_DIVERGENT; } - if (peer.getVersion() >= 2) { - step <<= 1; - } else { - // Old v1 peers are hard-coded to return 500 signatures so we might as well go backward by 500 too - step = 500; - } + step <<= 1; step = Math.min(step, MAXIMUM_BLOCK_STEP); testHeight = Math.max(testHeight - step, 1); @@ -415,8 +409,7 @@ public class Synchronizer { } private List getBlockSignatures(Peer peer, byte[] parentSignature, int numberRequested) throws InterruptedException { - // numberRequested is v2+ feature - Message getSignaturesMessage = peer.getVersion() >= 2 ? new GetSignaturesV2Message(parentSignature, numberRequested) : new GetSignaturesMessage(parentSignature); + Message getSignaturesMessage = new GetSignaturesV2Message(parentSignature, numberRequested); Message message = peer.getResponse(getSignaturesMessage); if (message == null || message.getType() != MessageType.SIGNATURES) diff --git a/src/main/java/org/qortal/network/Handshake.java b/src/main/java/org/qortal/network/Handshake.java index d70e9cc4..8fb0fa2e 100644 --- a/src/main/java/org/qortal/network/Handshake.java +++ b/src/main/java/org/qortal/network/Handshake.java @@ -1,158 +1,193 @@ package org.qortal.network; import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qortal.controller.Controller; +import org.qortal.crypto.Crypto; +import org.qortal.crypto.MemoryPoW; +import org.qortal.network.message.ChallengeMessage; +import org.qortal.network.message.HelloMessage; import org.qortal.network.message.Message; -import org.qortal.network.message.PeerIdMessage; -import org.qortal.network.message.PeerVerifyMessage; -import org.qortal.network.message.ProofMessage; -import org.qortal.network.message.VerificationCodesMessage; -import org.qortal.network.message.VersionMessage; import org.qortal.network.message.Message.MessageType; +import org.qortal.network.message.ResponseMessage; +import org.qortal.utils.NTP; + +import com.google.common.primitives.Bytes; public enum Handshake { STARTED(null) { @Override public Handshake onMessage(Peer peer, Message message) { - return SELF_CHECK; + return HELLO; } @Override public void action(Peer peer) { } }, - SELF_CHECK(MessageType.PEER_ID) { + HELLO(MessageType.HELLO) { @Override public Handshake onMessage(Peer peer, Message message) { - PeerIdMessage peerIdMessage = (PeerIdMessage) message; - byte[] peerId = peerIdMessage.getPeerId(); + HelloMessage helloMessage = (HelloMessage) message; - if (Arrays.equals(peerId, Network.ZERO_PEER_ID)) { - if (peer.isOutbound()) { - // Peer has indicated they already have an outbound connection to us - LOGGER.trace(String.format("Peer %s already connected to us - discarding this connection", peer)); - } else { - // Not sure this should occur so log it - LOGGER.info(String.format("Inbound peer %s claims we also have outbound connection to them?", peer)); - } + long peersConnectionTimestamp = helloMessage.getTimestamp(); + long now = NTP.getTime(); + long timestampDelta = Math.abs(peersConnectionTimestamp - now); + 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; } - if (Arrays.equals(peerId, Network.getInstance().getOurPeerId())) { - // Connected to self! + String versionString = helloMessage.getVersionString(); + + Matcher matcher = VERSION_PATTERN.matcher(versionString); + if (!matcher.lookingAt()) { + 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 + long version = 0; + for (int g = 1; g <= 3; ++g) { + long value = Long.parseLong(matcher.group(g)); + + if (value < 0 || value > Short.MAX_VALUE) + return null; + + version <<= 16; + version |= value; + } + + peer.setPeersConnectionTimestamp(peersConnectionTimestamp); + peer.setPeersVersion(versionString, version); + + return CHALLENGE; + } + + @Override + public void action(Peer peer) { + String versionString = Controller.getInstance().getVersionString(); + long timestamp = NTP.getTime(); + + Message helloMessage = new HelloMessage(timestamp, versionString); + if (!peer.sendMessage(helloMessage)) + peer.disconnect("failed to send HELLO"); + } + }, + CHALLENGE(MessageType.CHALLENGE) { + @Override + public Handshake onMessage(Peer peer, Message message) { + ChallengeMessage challengeMessage = (ChallengeMessage) message; + + byte[] peersPublicKey = challengeMessage.getPublicKey(); + byte[] peersChallenge = challengeMessage.getChallenge(); + + // If public key matches our public key then we've connected to self + byte[] ourPublicKey = Network.getInstance().getOurPublicKey(); + if (Arrays.equals(ourPublicKey, peersPublicKey)) { // 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 deal with disconnect + // 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' - sendMyId(peer); - // We return SELF_CHECK here to prevent us from closing connection, which currently preempts - // remote end from reading any pending messages, specifically the PEER_ID 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 SELF_CHECK; + challengeMessage = new ChallengeMessage(ourPublicKey, ZERO_CHALLENGE); + if (!peer.sendMessage(challengeMessage)) + peer.disconnect("failed to send CHALLENGE to self"); + + /* + * 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; } } - // Is this ID already connected inbound or outbound? - Peer otherInboundPeer = Network.getInstance().getInboundPeerWithId(peerId); - Peer otherOutboundPeer = Network.getInstance().getOutboundHandshakedPeerWithId(peerId); - - // Extra checks on inbound peers with known IDs, to prevent ID stealing - if (!peer.isOutbound() && otherInboundPeer != null) { - if (otherOutboundPeer == null) { - // We already have an inbound peer with this ID, but no outgoing peer with which to request verification - LOGGER.trace(String.format("Discarding inbound peer %s with existing ID", peer)); - - // Let peer know by sending special zero peer ID. This avoids peer keeping connection open until timeout. - peerIdMessage = new PeerIdMessage(Network.ZERO_PEER_ID); - peer.sendMessage(peerIdMessage); - - return null; - } else { - // Use corresponding outbound peer to verify inbound - LOGGER.trace(String.format("We will be using outbound peer %s to verify inbound peer %s with same ID", otherOutboundPeer, peer)); - - // Discard peer's ID - // peer.setPeerId(peerId); - - // Generate verification codes for later - peer.generateVerificationCodes(); - } - } else if (peer.isOutbound() && otherOutboundPeer != null) { - // We already have an outbound connection to this peer? - LOGGER.info(String.format("We already have another outbound connection to peer %s - discarding", peer)); + // Are we already connected to this peer? + 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; - } else { - // Set peer's ID - peer.setPeerId(peerId); } - return VERSION; + peer.setPeersPublicKey(peersPublicKey); + peer.setPeersChallenge(peersChallenge); + + return RESPONSE; } @Override public void action(Peer peer) { - sendMyId(peer); + // Send challenge + byte[] publicKey = Network.getInstance().getOurPublicKey(); + byte[] challenge = peer.getOurChallenge(); + + Message challengeMessage = new ChallengeMessage(publicKey, challenge); + if (!peer.sendMessage(challengeMessage)) + peer.disconnect("failed to send CHALLENGE"); } }, - VERSION(MessageType.VERSION) { + RESPONSE(MessageType.RESPONSE) { @Override public Handshake onMessage(Peer peer, Message message) { - peer.setVersionMessage((VersionMessage) message); + ResponseMessage responseMessage = (ResponseMessage) message; - // If we're both version 2 peers then next stage is proof - if (peer.getVersion() >= 2) - return PROOF; + byte[] peersPublicKey = peer.getPeersPublicKey(); + byte[] ourChallenge = peer.getOurChallenge(); + + byte[] sharedSecret = Network.getInstance().getSharedSecret(peersPublicKey); + final byte[] expectedData = Crypto.digest(Bytes.concat(sharedSecret, ourChallenge)); + + byte[] data = responseMessage.getData(); + if (!Arrays.equals(expectedData, data)) { + LOGGER.debug(() -> String.format("Peer %s sent incorrect RESPONSE data", peer)); + return null; + } + + int nonce = responseMessage.getNonce(); + if (!MemoryPoW.verify2(data, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce)) { + LOGGER.debug(() -> String.format("Peer %s sent incorrect RESPONSE nonce", peer)); + return null; + } + + peer.setPeersNodeId(Crypto.toNodeAddress(peersPublicKey)); - // Fall-back for older clients (for now) return COMPLETED; } @Override public void action(Peer peer) { - sendVersion(peer); - } - }, - PROOF(MessageType.PROOF) { - @Override - public Handshake onMessage(Peer peer, Message message) { - ProofMessage proofMessage = (ProofMessage) message; + // Send response - // Check peer's timestamp is within acceptable bounds - if (Math.abs(proofMessage.getTimestamp() - peer.getConnectionTimestamp()) > MAX_TIMESTAMP_DELTA) { - LOGGER.debug(String.format("Rejecting PROOF from %s as timestamp delta %d greater than max %d", peer, Math.abs(proofMessage.getTimestamp() - peer.getConnectionTimestamp()), MAX_TIMESTAMP_DELTA)); - return null; - } + byte[] peersPublicKey = peer.getPeersPublicKey(); + byte[] peersChallenge = peer.getPeersChallenge(); - // Save peer's value for connectionTimestamp - peer.setPeersConnectionTimestamp(proofMessage.getTimestamp()); + byte[] sharedSecret = Network.getInstance().getSharedSecret(peersPublicKey); + final byte[] data = Crypto.digest(Bytes.concat(sharedSecret, peersChallenge)); - // If we connected outbound to peer, then this is a faked confirmation response, so we're good - if (peer.isOutbound()) - return COMPLETED; + // We do this in a new thread as it can take a while... + Thread responseThread = new Thread(() -> { + Integer nonce = MemoryPoW.compute2(data, POW_BUFFER_SIZE, POW_DIFFICULTY); - // Check salt hasn't been seen before - this stops multiple peers reusing same nonce in a Sybil-like attack - if (Proof.seenSalt(proofMessage.getSalt())) - return null; + Message responseMessage = new ResponseMessage(nonce, data); + if (!peer.sendMessage(responseMessage)) + peer.disconnect("failed to send RESPONSE"); + }); - if (!Proof.check(proofMessage.getTimestamp(), proofMessage.getSalt(), proofMessage.getNonce())) - return null; - - // Proof valid - return COMPLETED; - } - - @Override - public void action(Peer peer) { - sendProof(peer); + responseThread.setDaemon(true); + responseThread.start(); } }, COMPLETED(null) { @@ -166,40 +201,6 @@ public enum Handshake { public void action(Peer peer) { // Note: this is only called when we've made outbound connection } - }, - PEER_VERIFY(null) { - @Override - public Handshake onMessage(Peer peer, Message message) { - // We only accept PEER_VERIFY messages - if (message.getType() != Message.MessageType.PEER_VERIFY) - return PEER_VERIFY; - - // Check returned code against expected - PeerVerifyMessage peerVerifyMessage = (PeerVerifyMessage) message; - - if (!Arrays.equals(peerVerifyMessage.getVerificationCode(), peer.getVerificationCodeExpected())) - return null; - - // Drop other inbound peers with the same ID - for (Peer otherPeer : Network.getInstance().getConnectedPeers()) - if (!otherPeer.isOutbound() && otherPeer.getPeerId() != null && Arrays.equals(otherPeer.getPeerId(), peer.getPendingPeerId())) - otherPeer.disconnect("doppelganger"); - - // Tidy up - peer.setVerificationCodes(null, null); - peer.setPeerId(peer.getPendingPeerId()); - peer.setPendingPeerId(null); - - // Completed for real this time - return COMPLETED; - } - - @Override - public void action(Peer peer) { - // Send VERIFICATION_CODE to other peer (that we connected to) - // Send PEER_VERIFY to peer - sendVerificationCodes(peer); - } }; private static final Logger LOGGER = LogManager.getLogger(Handshake.class); @@ -207,6 +208,13 @@ public enum Handshake { /** Maximum allowed difference between peer's reported timestamp and when they connected, in milliseconds. */ private static final long MAX_TIMESTAMP_DELTA = 30 * 1000L; // ms + private static final Pattern VERSION_PATTERN = Pattern.compile(Controller.VERSION_PREFIX + "(\\d{1,3})\\.(\\d{1,5})\\.(\\d{1,5})"); + + private static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes + private static final int POW_DIFFICULTY = 12; // leading zero bits + + private static final byte[] ZERO_CHALLENGE = new byte[ChallengeMessage.CHALLENGE_LENGTH]; + public final MessageType expectedMessageType; private Handshake(MessageType expectedMessageType) { @@ -217,47 +225,4 @@ public enum Handshake { public abstract void action(Peer peer); - private static void sendVersion(Peer peer) { - long buildTimestamp = Controller.getInstance().getBuildTimestamp(); - String versionString = Controller.getInstance().getVersionString(); - - Message versionMessage = new VersionMessage(buildTimestamp, versionString); - if (!peer.sendMessage(versionMessage)) - peer.disconnect("failed to send version"); - } - - private static void sendMyId(Peer peer) { - Message peerIdMessage = new PeerIdMessage(Network.getInstance().getOurPeerId()); - if (!peer.sendMessage(peerIdMessage)) - peer.disconnect("failed to send peer ID"); - } - - private static void sendProof(Peer peer) { - if (peer.isOutbound()) { - // For outbound connections we need to generate real proof - new Proof(peer).start(); // Calculate & send in a new thread to free up networking processing - } else { - // For incoming connections we only need to send a fake proof message as confirmation - Message proofMessage = new ProofMessage(peer.getConnectionTimestamp(), 0, 0); - if (!peer.sendMessage(proofMessage)) - peer.disconnect("failed to send proof"); - } - } - - private static void sendVerificationCodes(Peer peer) { - Peer otherOutboundPeer = Network.getInstance().getOutboundHandshakedPeerWithId(peer.getPendingPeerId()); - - // Send VERIFICATION_CODES to peer - Message verificationCodesMessage = new VerificationCodesMessage(peer.getVerificationCodeSent(), peer.getVerificationCodeExpected()); - if (!otherOutboundPeer.sendMessage(verificationCodesMessage)) { - peer.disconnect("failed to send verification codes"); // give up with this peer instead - return; - } - - // Send PEER_VERIFY to peer - Message peerVerifyMessage = new PeerVerifyMessage(peer.getVerificationCodeSent()); - if (!peer.sendMessage(peerVerifyMessage)) - peer.disconnect("failed to send verification code"); - } - } diff --git a/src/main/java/org/qortal/network/Network.java b/src/main/java/org/qortal/network/Network.java index d27e3ba1..cf9fe5f0 100644 --- a/src/main/java/org/qortal/network/Network.java +++ b/src/main/java/org/qortal/network/Network.java @@ -1,7 +1,6 @@ package org.qortal.network; import java.io.IOException; -import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.StandardSocketOptions; @@ -32,28 +31,26 @@ import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters; +import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; import org.qortal.block.BlockChain; import org.qortal.controller.Controller; +import org.qortal.crypto.Crypto; import org.qortal.data.block.BlockData; import org.qortal.data.network.PeerData; import org.qortal.data.transaction.TransactionData; import org.qortal.network.message.GetPeersMessage; import org.qortal.network.message.GetUnconfirmedTransactionsMessage; -import org.qortal.network.message.HeightMessage; import org.qortal.network.message.HeightV2Message; import org.qortal.network.message.Message; -import org.qortal.network.message.PeerVerifyMessage; -import org.qortal.network.message.PeersMessage; import org.qortal.network.message.PeersV2Message; import org.qortal.network.message.PingMessage; -import org.qortal.network.message.TransactionMessage; import org.qortal.network.message.TransactionSignaturesMessage; -import org.qortal.network.message.VerificationCodesMessage; -import org.qortal.network.message.Message.MessageType; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; import org.qortal.settings.Settings; +import org.qortal.transform.Transformer; import org.qortal.utils.ExecuteProduceConsume; // import org.qortal.utils.ExecutorDumper; import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot; @@ -99,10 +96,11 @@ public class Network { public static final int MAX_SIGNATURES_PER_REPLY = 500; public static final int MAX_BLOCK_SUMMARIES_PER_REPLY = 500; - public static final int PEER_ID_LENGTH = 128; - public static final byte[] ZERO_PEER_ID = new byte[PEER_ID_LENGTH]; - private final byte[] ourPeerId; + private final Ed25519PrivateKeyParameters edPrivateKeyParams; + private final Ed25519PublicKeyParameters edPublicKeyParams; + private final String ourNodeId; + private final int maxMessageSize; private List allKnownPeers; @@ -129,10 +127,13 @@ public class Network { connectedPeers = new ArrayList<>(); selfPeers = new ArrayList<>(); - ourPeerId = new byte[PEER_ID_LENGTH]; - new SecureRandom().nextBytes(ourPeerId); - // Set bit to make sure our peer ID is not 0 - ourPeerId[ourPeerId.length - 1] |= 0x01; + // Generate our ID + byte[] seed = new byte[Transformer.PRIVATE_KEY_LENGTH]; + new SecureRandom().nextBytes(seed); + + edPrivateKeyParams = new Ed25519PrivateKeyParameters(seed, 0); + edPublicKeyParams = edPrivateKeyParams.generatePublicKey(); + ourNodeId = Crypto.toNodeAddress(edPublicKeyParams.getEncoded()); maxMessageSize = 4 + 1 + 4 + BlockChain.getInstance().getMaxBlockSize(); @@ -201,8 +202,12 @@ public class Network { return Settings.getInstance().isTestNet() ? TESTNET_MESSAGE_MAGIC : MAINNET_MESSAGE_MAGIC; } - public byte[] getOurPeerId() { - return this.ourPeerId; + public String getOurNodeId() { + return this.ourNodeId; + } + + /*package*/ byte[] getOurPublicKey() { + return this.edPublicKeyParams.getEncoded(); } /** Maximum message size (bytes). Needs to be at least maximum block size + MAGIC + message type, etc. */ @@ -241,25 +246,6 @@ public class Network { } } - /** Returns list of connected peers that have completed handshaking, with inbound duplicates removed. */ - public List getUniqueHandshakedPeers() { - List peers = getHandshakedPeers(); - - // Returns true if this peer is inbound and has corresponding outbound peer with same ID - Predicate hasOutboundWithSameId = peer -> { - // Peer is outbound so return fast - if (peer.isOutbound()) - return false; - - return peers.stream().anyMatch(otherPeer -> otherPeer.isOutbound() && Arrays.equals(otherPeer.getPeerId(), peer.getPeerId())); - }; - - // Filter out inbound peers that have corresponding outbound peer with the same ID - peers.removeIf(hasOutboundWithSameId); - - return peers; - } - /** Returns list of peers we connected to that have completed handshaking. */ public List getOutboundHandshakedPeers() { synchronized (this.connectedPeers) { @@ -267,21 +253,13 @@ public class Network { } } - /** Returns Peer with inbound connection and matching ID, or null if none found. */ - public Peer getInboundPeerWithId(byte[] peerId) { + /** Returns first peer that has completed handshaking and has matching public key. */ + public Peer getHandshakedPeerWithPublicKey(byte[] publicKey) { synchronized (this.connectedPeers) { - return this.connectedPeers.stream().filter(peer -> !peer.isOutbound() && peer.getPeerId() != null && Arrays.equals(peer.getPeerId(), peerId)).findAny().orElse(null); + return this.connectedPeers.stream().filter(peer -> peer.getHandshakeStatus() == Handshake.COMPLETED && Arrays.equals(peer.getPeersPublicKey(), publicKey)).findFirst().orElse(null); } } - /** Returns handshake-completed Peer with outbound connection and matching ID, or null if none found. */ - public Peer getOutboundHandshakedPeerWithId(byte[] peerId) { - synchronized (this.connectedPeers) { - return this.connectedPeers.stream().filter(peer -> peer.isOutbound() && peer.getHandshakeStatus() == Handshake.COMPLETED && peer.getPeerId() != null && Arrays.equals(peer.getPeerId(), peerId)).findAny().orElse(null); - } - } - - // Peer list filters /** Must be inside synchronized (this.selfPeers) {...} */ @@ -639,10 +617,22 @@ public class Network { // Peer callbacks - /* package */ void wakeupChannelSelector() { + /*package*/ void wakeupChannelSelector() { this.channelSelector.wakeup(); } + /*package*/ boolean verify(byte[] signature, byte[] message) { + return Crypto.verify(this.edPublicKeyParams.getEncoded(), signature, message); + } + + /*package*/ byte[] sign(byte[] message) { + return Crypto.sign(this.edPrivateKeyParams, message); + } + + /*package*/ byte[] getSharedSecret(byte[] publicKey) { + return Crypto.getSharedSecret(this.edPrivateKeyParams.getEncoded(), publicKey); + } + /** Called when Peer's thread has setup and is ready to process messages */ public void onPeerReady(Peer peer) { this.onMessage(peer, null); @@ -692,17 +682,13 @@ public class Network { onGetPeersMessage(peer, message); break; - case PEERS: - onPeersMessage(peer, message); - break; - case PING: onPingMessage(peer, message); break; - case VERSION: - case PEER_ID: - case PROOF: + case HELLO: + case CHALLENGE: + case RESPONSE: LOGGER.debug(() -> String.format("Unexpected handshaking message %s from peer %s", message.getType().name(), peer)); peer.disconnect("unexpected handshaking message"); return; @@ -711,14 +697,6 @@ public class Network { onPeersV2Message(peer, message); break; - case PEER_VERIFY: - onPeerVerifyMessage(peer, message); - break; - - case VERIFICATION_CODES: - onVerificationCodesMessage(peer, message); - break; - default: // Bump up to controller for possible action Controller.getInstance().onNetworkMessage(peer, message); @@ -731,12 +709,6 @@ public class Network { // Still handshaking LOGGER.trace(() -> String.format("Handshake status %s, message %s from peer %s", handshakeStatus.name(), (message != null ? message.getType().name() : "null"), peer)); - // v1 nodes are keen on sending PINGs early. Send to back of queue so we'll process right after handshake - if (message != null && message.getType() == MessageType.PING) { - peer.queueMessage(message); - return; - } - // Check message type is as expected if (handshakeStatus.expectedMessageType != null && message.getType() != handshakeStatus.expectedMessageType) { LOGGER.debug(() -> String.format("Unexpected %s message from %s, expected %s", message.getType().name(), peer, handshakeStatus.expectedMessageType)); @@ -775,22 +747,6 @@ public class Network { peer.disconnect("failed to send peers list"); } - private void onPeersMessage(Peer peer, Message message) { - PeersMessage peersMessage = (PeersMessage) message; - - List peerAddresses = new ArrayList<>(); - - // v1 PEERS message doesn't support port numbers so we have to add default port - for (InetAddress peerAddress : peersMessage.getPeerAddresses()) - // This is always IPv4 so we don't have to worry about bracketing IPv6. - peerAddresses.add(PeerAddress.fromString(peerAddress.getHostAddress())); - - // Also add peer's details - peerAddresses.add(PeerAddress.fromString(peer.getPeerData().getAddress().getHost())); - - opportunisticMergePeers(peer.toString(), peerAddresses); - } - private void onPingMessage(Peer peer, Message message) { PingMessage pingMessage = (PingMessage) message; @@ -821,45 +777,7 @@ public class Network { opportunisticMergePeers(peer.toString(), peerV2Addresses); } - private void onPeerVerifyMessage(Peer peer, Message message) { - // Remote peer wants extra verification - possibleVerificationResponse(peer); - } - - private void onVerificationCodesMessage(Peer peer, Message message) { - VerificationCodesMessage verificationCodesMessage = (VerificationCodesMessage) message; - - // Remote peer is sending the code it wants to receive back via our outbound connection to it - Peer ourUnverifiedPeer = Network.getInstance().getInboundPeerWithId(Network.getInstance().getOurPeerId()); - ourUnverifiedPeer.setVerificationCodes(verificationCodesMessage.getVerificationCodeSent(), verificationCodesMessage.getVerificationCodeExpected()); - - possibleVerificationResponse(ourUnverifiedPeer); - } - - private void possibleVerificationResponse(Peer peer) { - // Can't respond if we don't have the codes (yet?) - if (peer.getVerificationCodeExpected() == null) - return; - - PeerVerifyMessage peerVerifyMessage = new PeerVerifyMessage(peer.getVerificationCodeExpected()); - if (!peer.sendMessage(peerVerifyMessage)) { - peer.disconnect("failed to send verification code"); - return; - } - - peer.setVerificationCodes(null, null); - peer.setHandshakeStatus(Handshake.COMPLETED); - this.onHandshakeCompleted(peer); - } - private void onHandshakeCompleted(Peer peer) { - // Do we need extra handshaking because of peer doppelgangers? - if (peer.getPendingPeerId() != null) { - peer.setHandshakeStatus(Handshake.PEER_VERIFY); - peer.getHandshakeStatus().action(peer); - return; - } - LOGGER.debug(String.format("Handshake completed with peer %s", peer)); // Make a note that we've successfully completed handshake (and when) @@ -930,86 +848,44 @@ public class Network { }; knownPeers.removeIf(notRecentlyConnected); - if (peer.getVersion() >= 2) { - List peerAddresses = new ArrayList<>(); + List peerAddresses = new ArrayList<>(); - for (PeerData peerData : knownPeers) { - try { - InetAddress address = InetAddress.getByName(peerData.getAddress().getHost()); + for (PeerData peerData : knownPeers) { + try { + InetAddress address = InetAddress.getByName(peerData.getAddress().getHost()); - // Don't send 'local' addresses if peer is not 'local'. e.g. don't send localhost:9084 to node4.qortal.org - if (!peer.getIsLocal() && Peer.isAddressLocal(address)) - continue; + // Don't send 'local' addresses if peer is not 'local'. e.g. don't send localhost:9084 to node4.qortal.org + if (!peer.isLocal() && Peer.isAddressLocal(address)) + continue; - peerAddresses.add(peerData.getAddress()); - } catch (UnknownHostException e) { - // Couldn't resolve hostname to IP address so discard - } + peerAddresses.add(peerData.getAddress()); + } catch (UnknownHostException e) { + // Couldn't resolve hostname to IP address so discard } - - // New format PEERS_V2 message that supports hostnames, IPv6 and ports - return new PeersV2Message(peerAddresses); - } else { - // Map to socket addresses - List peerAddresses = new ArrayList<>(); - - for (PeerData peerData : knownPeers) { - try { - // We have to resolve to literal IP address to check for IPv4-ness. - // This isn't great if hostnames have both IPv6 and IPv4 DNS entries. - InetAddress address = InetAddress.getByName(peerData.getAddress().getHost()); - - // Legacy PEERS message doesn't support IPv6 - if (address instanceof Inet6Address) - continue; - - // Don't send 'local' addresses if peer is not 'local'. e.g. don't send localhost:9084 to node4.qortal.org - if (!peer.getIsLocal() && !Peer.isAddressLocal(address)) - continue; - - peerAddresses.add(address); - } catch (UnknownHostException e) { - // Couldn't resolve hostname to IP address so discard - } - } - - // Legacy PEERS message that only sends IPv4 addresses - return new PeersMessage(peerAddresses); } + + // New format PEERS_V2 message that supports hostnames, IPv6 and ports + return new PeersV2Message(peerAddresses); } public Message buildHeightMessage(Peer peer, BlockData blockData) { - if (peer.getVersion() < 2) { - // Legacy height message - return new HeightMessage(blockData.getHeight()); - } - // HEIGHT_V2 contains way more useful info return new HeightV2Message(blockData.getHeight(), blockData.getSignature(), blockData.getTimestamp(), blockData.getMinterPublicKey()); } public Message buildNewTransactionMessage(Peer peer, TransactionData transactionData) { - if (peer.getVersion() < 2) { - // Legacy TRANSACTION message - return new TransactionMessage(transactionData); - } - // In V2 we send out transaction signature only and peers can decide whether to request the full transaction return new TransactionSignaturesMessage(Collections.singletonList(transactionData.getSignature())); } public Message buildGetUnconfirmedTransactionsMessage(Peer peer) { - // V2 only - if (peer.getVersion() < 2) - return null; - return new GetUnconfirmedTransactionsMessage(); } // Peer-management calls public void noteToSelf(Peer peer) { - LOGGER.info(String.format("No longer considering peer address %s as it connects to self", peer)); + LOGGER.info(() -> String.format("No longer considering peer address %s as it connects to self", peer)); synchronized (this.selfPeers) { this.selfPeers.add(peer.getPeerData().getAddress()); @@ -1230,7 +1106,7 @@ public class Network { } try { - broadcastExecutor.execute(new Broadcaster(this.getUniqueHandshakedPeers(), peerMessageBuilder)); + broadcastExecutor.execute(new Broadcaster(this.getHandshakedPeers(), peerMessageBuilder)); } catch (RejectedExecutionException e) { // Can't execute - probably because we're shutting down, so ignore } diff --git a/src/main/java/org/qortal/network/Peer.java b/src/main/java/org/qortal/network/Peer.java index 419aa415..968b9e51 100644 --- a/src/main/java/org/qortal/network/Peer.java +++ b/src/main/java/org/qortal/network/Peer.java @@ -24,9 +24,9 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qortal.data.network.PeerChainTipData; import org.qortal.data.network.PeerData; +import org.qortal.network.message.ChallengeMessage; import org.qortal.network.message.Message; import org.qortal.network.message.PingMessage; -import org.qortal.network.message.VersionMessage; import org.qortal.network.message.Message.MessageException; import org.qortal.network.message.Message.MessageType; import org.qortal.settings.Settings; @@ -42,7 +42,7 @@ public class Peer { private static final Logger LOGGER = LogManager.getLogger(Peer.class); /** Maximum time to allow connect() to remote peer to complete. (ms) */ - private static final int CONNECT_TIMEOUT = 1000; // ms + private static final int CONNECT_TIMEOUT = 2000; // ms /** Maximum time to wait for a message reply to arrive from peer. (ms) */ private static final int RESPONSE_TIMEOUT = 2000; // ms @@ -54,9 +54,6 @@ public class Peer { */ private static final int PING_INTERVAL = 20_000; // ms - /** Threshold for buildTimestamp in VERSION messages where we consider peer to be using v2 protocol. */ - private static final long V2_PROTOCOL_TIMESTAMP_THRESHOLD = 1546300800L; // midnight starting 1st Jan 2019 - private volatile boolean isStopping = false; private SocketChannel socketChannel = null; @@ -65,41 +62,48 @@ public class Peer { private boolean isLocal; private final Object byteBufferLock = new Object(); - private volatile ByteBuffer byteBuffer; + private ByteBuffer byteBuffer; + private Map> replyQueues; private LinkedBlockingQueue pendingMessages; /** True if we created connection to peer, false if we accepted incoming connection from peer. */ private final boolean isOutbound; - /** Numeric protocol version, typically 1 or 2. */ - private volatile Integer version; - private volatile byte[] peerId; - private volatile Handshake handshakeStatus = Handshake.STARTED; + private final Object handshakingLock = new Object(); + private Handshake handshakeStatus = Handshake.STARTED; private volatile boolean handshakeMessagePending = false; - private volatile byte[] pendingPeerId; - private volatile byte[] verificationCodeSent; - private volatile byte[] verificationCodeExpected; - - private volatile PeerData peerData = null; - /** Timestamp of when socket was accepted, or connected. */ - private volatile Long connectionTimestamp = null; - - /** Peer's value of connectionTimestamp. */ - private volatile Long peersConnectionTimestamp = null; - - /** Version info as reported by peer. */ - private volatile VersionMessage versionMessage = null; + private Long connectionTimestamp = null; /** Last PING message round-trip time (ms). */ - private volatile Long lastPing = null; + private Long lastPing = null; /** When last PING message was sent, or null if pings not started yet. */ - private volatile Long lastPingSent; + private Long lastPingSent; + + byte[] ourChallenge; + + // Peer info + + private final Object peerInfoLock = new Object(); + + private String peersNodeId; + private byte[] peersPublicKey; + private byte[] peersChallenge; + + private PeerData peerData = null; + + /** Peer's value of connectionTimestamp. */ + private Long peersConnectionTimestamp = null; + + /** Version string as reported by peer. */ + private String peersVersionString = null; + /** Numeric version of peer. */ + private Long peersVersion = null; /** Latest block info as reported by peer. */ - private volatile PeerChainTipData chainTipData; + private PeerChainTipData peersChainTipData; // Constructors @@ -124,16 +128,20 @@ public class Peer { // Getters / setters - public SocketChannel getSocketChannel() { - return this.socketChannel; - } - public boolean isStopping() { return this.isStopping; } - public PeerData getPeerData() { - return this.peerData; + public SocketChannel getSocketChannel() { + return this.socketChannel; + } + + public InetSocketAddress getResolvedAddress() { + return this.resolvedAddress; + } + + public boolean isLocal() { + return this.isLocal; } public boolean isOutbound() { @@ -141,103 +149,131 @@ public class Peer { } public Handshake getHandshakeStatus() { - return this.handshakeStatus; - } - - public void setHandshakeStatus(Handshake handshakeStatus) { - this.handshakeStatus = handshakeStatus; - } - - public void resetHandshakeMessagePending() { - this.handshakeMessagePending = false; - } - - public VersionMessage getVersionMessage() { - return this.versionMessage; - } - - public void setVersionMessage(VersionMessage versionMessage) { - this.versionMessage = versionMessage; - - if (this.versionMessage.getBuildTimestamp() >= V2_PROTOCOL_TIMESTAMP_THRESHOLD) { - this.version = 2; // enhanced protocol - } else { - this.version = 1; // legacy protocol + synchronized (this.handshakingLock) { + return this.handshakeStatus; } } - public Integer getVersion() { - return this.version; + /*package*/ void setHandshakeStatus(Handshake handshakeStatus) { + synchronized (this.handshakingLock) { + this.handshakeStatus = handshakeStatus; + } + } + + /*package*/ void resetHandshakeMessagePending() { + this.handshakeMessagePending = false; + } + + public PeerData getPeerData() { + synchronized (this.peerInfoLock) { + return this.peerData; + } } public Long getConnectionTimestamp() { - return this.connectionTimestamp; + synchronized (this.peerInfoLock) { + return this.connectionTimestamp; + } + } + + public String getPeersVersionString() { + synchronized (this.peerInfoLock) { + return this.peersVersionString; + } + } + + public Long getPeersVersion() { + synchronized (this.peerInfoLock) { + return this.peersVersion; + } + } + + /*package*/ void setPeersVersion(String versionString, long version) { + synchronized (this.peerInfoLock) { + this.peersVersionString = versionString; + this.peersVersion = version; + } } public Long getPeersConnectionTimestamp() { - return this.peersConnectionTimestamp; + synchronized (this.peerInfoLock) { + return this.peersConnectionTimestamp; + } } - /* package */ void setPeersConnectionTimestamp(Long peersConnectionTimestamp) { - this.peersConnectionTimestamp = peersConnectionTimestamp; + /*package*/ void setPeersConnectionTimestamp(Long peersConnectionTimestamp) { + synchronized (this.peerInfoLock) { + this.peersConnectionTimestamp = peersConnectionTimestamp; + } } public Long getLastPing() { - return this.lastPing; + synchronized (this.peerInfoLock) { + return this.lastPing; + } } - public void setLastPing(long lastPing) { - this.lastPing = lastPing; + /*package*/ void setLastPing(long lastPing) { + synchronized (this.peerInfoLock) { + this.lastPing = lastPing; + } } - public InetSocketAddress getResolvedAddress() { - return this.resolvedAddress; + /*package*/ byte[] getOurChallenge() { + return this.ourChallenge; } - public boolean getIsLocal() { - return this.isLocal; + public String getPeersNodeId() { + synchronized (this.peerInfoLock) { + return this.peersNodeId; + } } - public byte[] getPeerId() { - return this.peerId; + /*package*/ void setPeersNodeId(String peersNodeId) { + synchronized (this.peerInfoLock) { + this.peersNodeId = peersNodeId; + } } - public void setPeerId(byte[] peerId) { - this.peerId = peerId; + public byte[] getPeersPublicKey() { + synchronized (this.peerInfoLock) { + return this.peersPublicKey; + } } - public byte[] getPendingPeerId() { - return this.pendingPeerId; + /*package*/ void setPeersPublicKey(byte[] peerPublicKey) { + synchronized (this.peerInfoLock) { + this.peersPublicKey = peerPublicKey; + } } - public void setPendingPeerId(byte[] peerId) { - this.pendingPeerId = peerId; + public byte[] getPeersChallenge() { + synchronized (this.peerInfoLock) { + return this.peersChallenge; + } } - public byte[] getVerificationCodeSent() { - return this.verificationCodeSent; - } - - public byte[] getVerificationCodeExpected() { - return this.verificationCodeExpected; - } - - public void setVerificationCodes(byte[] sent, byte[] expected) { - this.verificationCodeSent = sent; - this.verificationCodeExpected = expected; + /*package*/ void setPeersChallenge(byte[] peersChallenge) { + synchronized (this.peerInfoLock) { + this.peersChallenge = peersChallenge; + } } public PeerChainTipData getChainTipData() { - return this.chainTipData; + synchronized (this.peerInfoLock) { + return this.peersChainTipData; + } } public void setChainTipData(PeerChainTipData chainTipData) { - this.chainTipData = chainTipData; + synchronized (this.peerInfoLock) { + this.peersChainTipData = chainTipData; + } } - /* package */ void queueMessage(Message message) { + /*package*/ void queueMessage(Message message) { if (!this.pendingMessages.offer(message)) - LOGGER.info(String.format("No room to queue message from peer %s - discarding", this)); + LOGGER.info(() -> String.format("No room to queue message from peer %s - discarding", this)); } @Override @@ -248,14 +284,6 @@ public class Peer { // Processing - public void generateVerificationCodes() { - verificationCodeSent = new byte[Network.PEER_ID_LENGTH]; - new SecureRandom().nextBytes(verificationCodeSent); - - verificationCodeExpected = new byte[Network.PEER_ID_LENGTH]; - new SecureRandom().nextBytes(verificationCodeExpected); - } - private void sharedSetup(Selector channelSelector) throws IOException { this.connectionTimestamp = NTP.getTime(); this.socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true); @@ -264,6 +292,10 @@ public class Peer { this.byteBuffer = null; // Defer allocation to when we need it, to save memory. Sorry GC! this.replyQueues = Collections.synchronizedMap(new HashMap>()); this.pendingMessages = new LinkedBlockingQueue<>(); + + Random random = new SecureRandom(); + this.ourChallenge = new byte[ChallengeMessage.CHALLENGE_LENGTH]; + random.nextBytes(this.ourChallenge); } public SocketChannel connect(Selector channelSelector) { @@ -378,9 +410,11 @@ public class Peer { } /* package */ ExecuteProduceConsume.Task getMessageTask() { - // If we are still handshaking and there is a message yet to be processed - // then don't produce another message task. - // This allows us to process handshake messages sequentially. + /* + * If we are still handshaking and there is a message yet to be processed then + * don't produce another message task. This allows us to process handshake + * messages sequentially. + */ if (this.handshakeMessagePending) return null; @@ -454,9 +488,10 @@ public class Peer { BlockingQueue blockingQueue = new ArrayBlockingQueue<>(1); // Assign random ID to this message + Random random = new Random(); int id; do { - id = new Random().nextInt(Integer.MAX_VALUE - 1) + 1; + id = random.nextInt(Integer.MAX_VALUE - 1) + 1; // Put queue into map (keyed by message ID) so we can poll for a response // If putIfAbsent() doesn't return null, then this ID is already taken diff --git a/src/main/java/org/qortal/network/Proof.java b/src/main/java/org/qortal/network/Proof.java deleted file mode 100644 index 64fdd29d..00000000 --- a/src/main/java/org/qortal/network/Proof.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.qortal.network; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.HashSet; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.qortal.network.message.ProofMessage; - -import com.google.common.primitives.Longs; - -public class Proof extends Thread { - - private static final int MIN_PROOF_ZEROS = 2; - private static final HashSet seenSalts = new HashSet<>(); - private static final Logger LOGGER = LogManager.getLogger(Proof.class); - - private Peer peer; - - public Proof(Peer peer) { - this.peer = peer; - setDaemon(true); - } - - public static boolean seenSalt(long salt) { - synchronized (seenSalts) { - return seenSalts.contains(salt); - } - } - - public static void addSalt(long salt) { - synchronized (seenSalts) { - seenSalts.add(salt); - } - } - - @Override - public void run() { - setName("Proof for peer " + this.peer); - - // Do proof-of-work calculation to gain acceptance with remote end - final long startTime = LOGGER.isTraceEnabled() ? System.currentTimeMillis() : 0; - - // Remote end knows this (approximately) - long timestamp = this.peer.getConnectionTimestamp(); - - // Needs to be unique on the remote end - long salt = new SecureRandom().nextLong(); - - byte[] message = new byte[8 + 8 + 8]; // nonce + salt + timestamp - - byte[] saltBytes = Longs.toByteArray(salt); - System.arraycopy(saltBytes, 0, message, 8, saltBytes.length); - - byte[] timestampBytes = Longs.toByteArray(timestamp); - System.arraycopy(timestampBytes, 0, message, 8 + 8, timestampBytes.length); - - MessageDigest sha256; - try { - sha256 = MessageDigest.getInstance("SHA256"); - } catch (NoSuchAlgorithmException e) { - // Can't progress - throw new RuntimeException("Message digest SHA256 not available"); - } - - long nonce; - for (nonce = 0; nonce < Long.MAX_VALUE; ++nonce) { - // Check whether we're shutting down every so often - if ((nonce & 0xff) == 0 && (this.peer.isStopping() || Thread.currentThread().isInterrupted())) - // throw new InterruptedException("Interrupted during peer proof calculation"); - return; - - byte[] nonceBytes = Longs.toByteArray(nonce); - System.arraycopy(nonceBytes, 0, message, 0, nonceBytes.length); - - byte[] digest = sha256.digest(message); - - if (check(digest)) - break; - - sha256.reset(); - } - - LOGGER.trace(() -> String.format("Proof for peer %s took %dms", this.peer, System.currentTimeMillis() - startTime)); - - ProofMessage proofMessage = new ProofMessage(timestamp, salt, nonce); - peer.sendMessage(proofMessage); - } - - private static boolean check(byte[] digest) { - int idx; - for (idx = 0; idx < MIN_PROOF_ZEROS; ++idx) - if (digest[idx] != 0) - break; - - return idx == MIN_PROOF_ZEROS; - } - - public static boolean check(long timestamp, long salt, long nonce) { - byte[] message = new byte[8 + 8 + 8]; - - byte[] saltBytes = Longs.toByteArray(salt); - System.arraycopy(saltBytes, 0, message, 8, saltBytes.length); - - byte[] timestampBytes = Longs.toByteArray(timestamp); - System.arraycopy(timestampBytes, 0, message, 8 + 8, timestampBytes.length); - - byte[] nonceBytes = Longs.toByteArray(nonce); - System.arraycopy(nonceBytes, 0, message, 0, nonceBytes.length); - - MessageDigest sha256; - try { - sha256 = MessageDigest.getInstance("SHA256"); - } catch (NoSuchAlgorithmException e) { - // Can't progress - throw new RuntimeException("Message digest SHA256 not available"); - } - - byte[] digest = sha256.digest(message); - - return check(digest); - } - -} diff --git a/src/main/java/org/qortal/network/message/ChallengeMessage.java b/src/main/java/org/qortal/network/message/ChallengeMessage.java new file mode 100644 index 00000000..425f9790 --- /dev/null +++ b/src/main/java/org/qortal/network/message/ChallengeMessage.java @@ -0,0 +1,56 @@ +package org.qortal.network.message; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.qortal.transform.Transformer; + +public class ChallengeMessage extends Message { + + public static final int CHALLENGE_LENGTH = 32; + + private final byte[] publicKey; + private final byte[] challenge; + + private ChallengeMessage(int id, byte[] publicKey, byte[] challenge) { + super(id, MessageType.CHALLENGE); + + this.publicKey = publicKey; + this.challenge = challenge; + } + + public ChallengeMessage(byte[] publicKey, byte[] challenge) { + this(-1, publicKey, challenge); + } + + public byte[] getPublicKey() { + return this.publicKey; + } + + public byte[] getChallenge() { + return this.challenge; + } + + public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) { + byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH]; + byteBuffer.get(publicKey); + + byte[] challenge = new byte[CHALLENGE_LENGTH]; + byteBuffer.get(challenge); + + return new ChallengeMessage(id, publicKey, challenge); + } + + @Override + protected byte[] toData() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(this.publicKey); + + bytes.write(this.challenge); + + return bytes.toByteArray(); + } + +} diff --git a/src/main/java/org/qortal/network/message/GetSignaturesMessage.java b/src/main/java/org/qortal/network/message/GetSignaturesMessage.java deleted file mode 100644 index 5d1a9ccf..00000000 --- a/src/main/java/org/qortal/network/message/GetSignaturesMessage.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.qortal.network.message; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; - -import org.qortal.transform.block.BlockTransformer; - -public class GetSignaturesMessage extends Message { - - private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH; - - private byte[] parentSignature; - - public GetSignaturesMessage(byte[] parentSignature) { - this(-1, parentSignature); - } - - private GetSignaturesMessage(int id, byte[] parentSignature) { - super(id, MessageType.GET_SIGNATURES); - - this.parentSignature = parentSignature; - } - - public byte[] getParentSignature() { - return this.parentSignature; - } - - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { - if (bytes.remaining() != BLOCK_SIGNATURE_LENGTH) - return null; - - byte[] parentSignature = new byte[BLOCK_SIGNATURE_LENGTH]; - - bytes.get(parentSignature); - - return new GetSignaturesMessage(id, parentSignature); - } - - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - bytes.write(this.parentSignature); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - -} diff --git a/src/main/java/org/qortal/network/message/GoodbyeMessage.java b/src/main/java/org/qortal/network/message/GoodbyeMessage.java new file mode 100644 index 00000000..75864060 --- /dev/null +++ b/src/main/java/org/qortal/network/message/GoodbyeMessage.java @@ -0,0 +1,65 @@ +package org.qortal.network.message; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toMap; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Map; + +import com.google.common.primitives.Ints; + +public class GoodbyeMessage extends Message { + + public enum Reason { + NO_HELLO(1), + BAD_HELLO(2), + BAD_HELLO_VERSION(3), + BAD_HELLO_TIMESTAMP(4); + + public final int value; + + private static final Map map = stream(Reason.values()) + .collect(toMap(reason -> reason.value, reason -> reason)); + + private Reason(int value) { + this.value = value; + } + + public static Reason valueOf(int value) { + return map.get(value); + } + } + + private final Reason reason; + + private GoodbyeMessage(int id, Reason reason) { + super(id, MessageType.GOODBYE); + + this.reason = reason; + } + + public GoodbyeMessage(Reason reason) { + this(-1, reason); + } + + public Reason getReason() { + return this.reason; + } + + public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) { + int reasonValue = byteBuffer.getInt(); + + Reason reason = Reason.valueOf(reasonValue); + if (reason == null) + return null; + + return new GoodbyeMessage(id, reason); + } + + @Override + protected byte[] toData() throws IOException { + return Ints.toByteArray(this.reason.value); + } + +} diff --git a/src/main/java/org/qortal/network/message/HeightMessage.java b/src/main/java/org/qortal/network/message/HeightMessage.java deleted file mode 100644 index 36b0d012..00000000 --- a/src/main/java/org/qortal/network/message/HeightMessage.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.qortal.network.message; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; - -import com.google.common.primitives.Ints; - -public class HeightMessage extends Message { - - private int height; - - public HeightMessage(int height) { - this(-1, height); - } - - private HeightMessage(int id, int height) { - super(id, MessageType.HEIGHT); - - this.height = height; - } - - public int getHeight() { - return this.height; - } - - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { - int height = bytes.getInt(); - - return new HeightMessage(id, height); - } - - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - bytes.write(Ints.toByteArray(this.height)); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - -} diff --git a/src/main/java/org/qortal/network/message/HelloMessage.java b/src/main/java/org/qortal/network/message/HelloMessage.java new file mode 100644 index 00000000..537daf48 --- /dev/null +++ b/src/main/java/org/qortal/network/message/HelloMessage.java @@ -0,0 +1,55 @@ +package org.qortal.network.message; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.qortal.transform.TransformationException; +import org.qortal.utils.Serialization; + +import com.google.common.primitives.Longs; + +public class HelloMessage extends Message { + + private final long timestamp; + private final String versionString; + + private HelloMessage(int id, long timestamp, String versionString) { + super(id, MessageType.HELLO); + + this.timestamp = timestamp; + this.versionString = versionString; + } + + public HelloMessage(long timestamp, String versionString) { + this(-1, timestamp, versionString); + } + + public long getTimestamp() { + return this.timestamp; + } + + public String getVersionString() { + return this.versionString; + } + + public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws TransformationException { + long timestamp = byteBuffer.getLong(); + + String versionString = Serialization.deserializeSizedString(byteBuffer, 255); + + return new HelloMessage(id, timestamp, versionString); + } + + @Override + protected byte[] toData() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Longs.toByteArray(this.timestamp)); + + Serialization.serializeSizedString(bytes, this.versionString); + + return bytes.toByteArray(); + } + +} diff --git a/src/main/java/org/qortal/network/message/Message.java b/src/main/java/org/qortal/network/message/Message.java index 4645b9e0..9dfdc6bc 100644 --- a/src/main/java/org/qortal/network/message/Message.java +++ b/src/main/java/org/qortal/network/message/Message.java @@ -4,6 +4,7 @@ import java.util.Map; import org.qortal.crypto.Crypto; import org.qortal.network.Network; +import org.qortal.transform.TransformationException; import com.google.common.primitives.Ints; @@ -45,32 +46,41 @@ public abstract class Message { } public enum MessageType { - GET_PEERS(1), - PEERS(2), - HEIGHT(3), - GET_SIGNATURES(4), - SIGNATURES(5), - GET_BLOCK(6), - BLOCK(7), - TRANSACTION(8), - PING(9), - VERSION(10), - PEER_ID(11), - PROOF(12), - PEERS_V2(13), - GET_BLOCK_SUMMARIES(14), - BLOCK_SUMMARIES(15), - GET_SIGNATURES_V2(16), - PEER_VERIFY(17), - VERIFICATION_CODES(18), - HEIGHT_V2(19), - GET_TRANSACTION(20), - GET_UNCONFIRMED_TRANSACTIONS(21), - TRANSACTION_SIGNATURES(22), - GET_ARBITRARY_DATA(23), - ARBITRARY_DATA(24), - GET_ONLINE_ACCOUNTS(25), - ONLINE_ACCOUNTS(26); + // Handshaking + HELLO(0), + GOODBYE(1), + CHALLENGE(2), + RESPONSE(3), + + // Status / notifications + HEIGHT_V2(10), + PING(11), + PONG(12), + + // Requesting data + PEERS_V2(20), + GET_PEERS(21), + + TRANSACTION(30), + GET_TRANSACTION(31), + + TRANSACTION_SIGNATURES(40), + GET_UNCONFIRMED_TRANSACTIONS(41), + + BLOCK(50), + GET_BLOCK(51), + + SIGNATURES(60), + GET_SIGNATURES_V2(61), + + BLOCK_SUMMARIES(70), + GET_BLOCK_SUMMARIES(71), + + ONLINE_ACCOUNTS(80), + GET_ONLINE_ACCOUNTS(81), + + ARBITRARY_DATA(90), + GET_ARBITRARY_DATA(91); public final int value; public final Method fromByteBufferMethod; @@ -263,11 +273,11 @@ public abstract class Message { throw new MessageException(String.format("About to send message with length %d larger than allowed %d", bytes.size(), MAX_DATA_SIZE)); return bytes.toByteArray(); - } catch (IOException e) { + } catch (IOException | TransformationException e) { throw new MessageException("Failed to serialize message", e); } } - protected abstract byte[] toData(); + protected abstract byte[] toData() throws IOException, TransformationException; } diff --git a/src/main/java/org/qortal/network/message/PeerIdMessage.java b/src/main/java/org/qortal/network/message/PeerIdMessage.java deleted file mode 100644 index bcb1246e..00000000 --- a/src/main/java/org/qortal/network/message/PeerIdMessage.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.qortal.network.message; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; - -import org.qortal.network.Network; - -public class PeerIdMessage extends Message { - - private byte[] peerId; - - public PeerIdMessage(byte[] peerId) { - this(-1, peerId); - } - - private PeerIdMessage(int id, byte[] peerId) { - super(id, MessageType.PEER_ID); - - this.peerId = peerId; - } - - public byte[] getPeerId() { - return this.peerId; - } - - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { - if (bytes.remaining() != Network.PEER_ID_LENGTH) - return null; - - byte[] peerId = new byte[Network.PEER_ID_LENGTH]; - - bytes.get(peerId); - - return new PeerIdMessage(id, peerId); - } - - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - bytes.write(this.peerId); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - -} diff --git a/src/main/java/org/qortal/network/message/PeerVerifyMessage.java b/src/main/java/org/qortal/network/message/PeerVerifyMessage.java deleted file mode 100644 index a77ce3c6..00000000 --- a/src/main/java/org/qortal/network/message/PeerVerifyMessage.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.qortal.network.message; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; - -import org.qortal.network.Network; - -public class PeerVerifyMessage extends Message { - - private byte[] verificationCode; - - public PeerVerifyMessage(byte[] verificationCode) { - this(-1, verificationCode); - } - - private PeerVerifyMessage(int id, byte[] verificationCode) { - super(id, MessageType.PEER_VERIFY); - - this.verificationCode = verificationCode; - } - - public byte[] getVerificationCode() { - return this.verificationCode; - } - - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { - if (bytes.remaining() != Network.PEER_ID_LENGTH) - return null; - - byte[] verificationCode = new byte[Network.PEER_ID_LENGTH]; - bytes.get(verificationCode); - - return new PeerVerifyMessage(id, verificationCode); - } - - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - bytes.write(this.verificationCode); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - -} diff --git a/src/main/java/org/qortal/network/message/PeersMessage.java b/src/main/java/org/qortal/network/message/PeersMessage.java deleted file mode 100644 index 45d8f837..00000000 --- a/src/main/java/org/qortal/network/message/PeersMessage.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.qortal.network.message; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -import com.google.common.primitives.Ints; - -// NOTE: this legacy message only supports 4-byte IPv4 addresses and doesn't send port number either -public class PeersMessage extends Message { - - private static final int ADDRESS_LENGTH = 4; - - private List peerAddresses; - - public PeersMessage(List peerAddresses) { - super(MessageType.PEERS); - - this.peerAddresses = new ArrayList<>(peerAddresses); - - // Legacy PEERS message doesn't support IPv6 - this.peerAddresses.removeIf(address -> address instanceof Inet6Address); - } - - private PeersMessage(int id, List peerAddresses) { - super(id, MessageType.PEERS); - - this.peerAddresses = peerAddresses; - } - - public List getPeerAddresses() { - return this.peerAddresses; - } - - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { - int count = bytes.getInt(); - - if (bytes.remaining() != count * ADDRESS_LENGTH) - return null; - - List peerAddresses = new ArrayList<>(); - - byte[] addressBytes = new byte[ADDRESS_LENGTH]; - - try { - for (int i = 0; i < count; ++i) { - bytes.get(addressBytes); - peerAddresses.add(InetAddress.getByAddress(addressBytes)); - } - } catch (UnknownHostException e) { - return null; - } - - return new PeersMessage(id, peerAddresses); - } - - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - bytes.write(Ints.toByteArray(this.peerAddresses.size())); - - for (InetAddress peerAddress : this.peerAddresses) - bytes.write(peerAddress.getAddress()); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - -} diff --git a/src/main/java/org/qortal/network/message/ProofMessage.java b/src/main/java/org/qortal/network/message/ProofMessage.java deleted file mode 100644 index dc67f948..00000000 --- a/src/main/java/org/qortal/network/message/ProofMessage.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.qortal.network.message; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; - -import com.google.common.primitives.Longs; - -public class ProofMessage extends Message { - - private long timestamp; - private long salt; - private long nonce; - - public ProofMessage(long timestamp, long salt, long nonce) { - this(-1, timestamp, salt, nonce); - } - - private ProofMessage(int id, long timestamp, long salt, long nonce) { - super(id, MessageType.PROOF); - - this.timestamp = timestamp; - this.salt = salt; - this.nonce = nonce; - } - - public long getTimestamp() { - return this.timestamp; - } - - public long getSalt() { - return this.salt; - } - - public long getNonce() { - return this.nonce; - } - - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { - long timestamp = bytes.getLong(); - long salt = bytes.getLong(); - long nonce = bytes.getLong(); - - return new ProofMessage(id, timestamp, salt, nonce); - } - - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - bytes.write(Longs.toByteArray(this.timestamp)); - bytes.write(Longs.toByteArray(this.salt)); - bytes.write(Longs.toByteArray(this.nonce)); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - -} diff --git a/src/main/java/org/qortal/network/message/ResponseMessage.java b/src/main/java/org/qortal/network/message/ResponseMessage.java new file mode 100644 index 00000000..6fed6d6a --- /dev/null +++ b/src/main/java/org/qortal/network/message/ResponseMessage.java @@ -0,0 +1,55 @@ +package org.qortal.network.message; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import com.google.common.primitives.Ints; + +public class ResponseMessage extends Message { + + public static final int DATA_LENGTH = 32; + + private final int nonce; + private final byte[] data; + + private ResponseMessage(int id, int nonce, byte[] data) { + super(id, MessageType.RESPONSE); + + this.nonce = nonce; + this.data = data; + } + + public ResponseMessage(int nonce, byte[] data) { + this(-1, nonce, data); + } + + public int getNonce() { + return this.nonce; + } + + public byte[] getData() { + return this.data; + } + + public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) { + int nonce = byteBuffer.getInt(); + + byte[] data = new byte[DATA_LENGTH]; + byteBuffer.get(data); + + return new ResponseMessage(id, nonce, data); + } + + @Override + protected byte[] toData() throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(4 + DATA_LENGTH); + + bytes.write(Ints.toByteArray(this.nonce)); + + bytes.write(data); + + return bytes.toByteArray(); + } + +} diff --git a/src/main/java/org/qortal/network/message/VerificationCodesMessage.java b/src/main/java/org/qortal/network/message/VerificationCodesMessage.java deleted file mode 100644 index b75e8930..00000000 --- a/src/main/java/org/qortal/network/message/VerificationCodesMessage.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.qortal.network.message; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; - -import org.qortal.network.Network; - -public class VerificationCodesMessage extends Message { - - private static final int TOTAL_LENGTH = Network.PEER_ID_LENGTH + Network.PEER_ID_LENGTH; - - private byte[] verificationCodeSent; - private byte[] verificationCodeExpected; - - public VerificationCodesMessage(byte[] verificationCodeSent, byte[] verificationCodeExpected) { - this(-1, verificationCodeSent, verificationCodeExpected); - } - - private VerificationCodesMessage(int id, byte[] verificationCodeSent, byte[] verificationCodeExpected) { - super(id, MessageType.VERIFICATION_CODES); - - this.verificationCodeSent = verificationCodeSent; - this.verificationCodeExpected = verificationCodeExpected; - } - - public byte[] getVerificationCodeSent() { - return this.verificationCodeSent; - } - - public byte[] getVerificationCodeExpected() { - return this.verificationCodeExpected; - } - - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { - if (bytes.remaining() != TOTAL_LENGTH) - return null; - - byte[] verificationCodeSent = new byte[Network.PEER_ID_LENGTH]; - bytes.get(verificationCodeSent); - - byte[] verificationCodeExpected = new byte[Network.PEER_ID_LENGTH]; - bytes.get(verificationCodeExpected); - - return new VerificationCodesMessage(id, verificationCodeSent, verificationCodeExpected); - } - - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - bytes.write(this.verificationCodeSent); - - bytes.write(this.verificationCodeExpected); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - -} diff --git a/src/main/java/org/qortal/network/message/VersionMessage.java b/src/main/java/org/qortal/network/message/VersionMessage.java deleted file mode 100644 index cdd8f514..00000000 --- a/src/main/java/org/qortal/network/message/VersionMessage.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.qortal.network.message; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; - -import org.qortal.utils.Serialization; - -import com.google.common.primitives.Longs; - -public class VersionMessage extends Message { - - private long buildTimestamp; - private String versionString; - - public VersionMessage(long buildTimestamp, String versionString) { - this(-1, buildTimestamp, versionString); - } - - private VersionMessage(int id, long buildTimestamp, String versionString) { - super(id, MessageType.VERSION); - - this.buildTimestamp = buildTimestamp; - this.versionString = versionString; - } - - public long getBuildTimestamp() { - return this.buildTimestamp; - } - - public String getVersionString() { - return this.versionString; - } - - public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { - long buildTimestamp = bytes.getLong(); - - int versionStringLength = bytes.getInt(); - - if (versionStringLength != bytes.remaining()) - return null; - - byte[] versionBytes = new byte[versionStringLength]; - bytes.get(versionBytes); - - String versionString = new String(versionBytes, StandardCharsets.UTF_8); - - return new VersionMessage(id, buildTimestamp, versionString); - } - - @Override - protected byte[] toData() { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - bytes.write(Longs.toByteArray(this.buildTimestamp)); - - Serialization.serializeSizedString(bytes, this.versionString); - - return bytes.toByteArray(); - } catch (IOException e) { - return null; - } - } - -}