From 8f7c954f5ad32f067ab633d8ab1884962df1585c Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 24 May 2019 12:39:24 +0100 Subject: [PATCH] Proxy private keys are now SHA256(shared secret only) instead of SHA256(shared secret + public keys). HTML/JS in src/test/resources/proxy-key-example.html updated accordingly. Add handshake status to output of API call GET /peers Add/correct @ApiErrors annotations on some API calls. Add API call POST /admin/orphan (target height as body) to force blockchain orphaning for when node is wildly out of sync. Added support for above to BlockChain class. BlockGenerator now requires a minimum number of peers before it will generate any new blocks. See "minBlockchainPeers" in settings. Controller now requires a minimum number of peers before it will consider synchronizing. See "minBlockchainPeers" in settings. Old "minPeers" entry in settings.json no longer valid! Networking now allows both an outbound and inbound connection to a peer although will use the outbound connection in preference. Networking checks peer ID of inbound connections to detect, and resolve, peer ID clashes/theft. --- .../org/qora/account/PrivateKeyAccount.java | 5 +- src/main/java/org/qora/api/ApiError.java | 1 + .../org/qora/api/model/ConnectedPeer.java | 5 +- .../org/qora/api/resource/AdminResource.java | 48 +++++++++- src/main/java/org/qora/block/BlockChain.java | 27 ++++++ .../java/org/qora/block/BlockGenerator.java | 5 ++ .../java/org/qora/controller/Controller.java | 4 +- .../org/qora/controller/Synchronizer.java | 4 +- src/main/java/org/qora/network/Handshake.java | 84 ++++++++++++++++-- src/main/java/org/qora/network/Network.java | 88 +++++++++++++++++-- src/main/java/org/qora/network/Peer.java | 33 +++++++ .../org/qora/network/message/Message.java | 4 +- .../network/message/PeerVerifyMessage.java | 51 +++++++++++ .../message/VerificationCodesMessage.java | 64 ++++++++++++++ src/main/java/org/qora/orphan.java | 14 +-- src/main/java/org/qora/settings/Settings.java | 14 ++- .../resources/i18n/ApiError_en.properties | 1 + src/test/java/org/qora/test/GuiTests.java | 17 ++++ src/test/resources/proxy-key-example.html | 14 +-- 19 files changed, 429 insertions(+), 54 deletions(-) create mode 100644 src/main/java/org/qora/network/message/PeerVerifyMessage.java create mode 100644 src/main/java/org/qora/network/message/VerificationCodesMessage.java create mode 100644 src/test/java/org/qora/test/GuiTests.java diff --git a/src/main/java/org/qora/account/PrivateKeyAccount.java b/src/main/java/org/qora/account/PrivateKeyAccount.java index d9f84cf4..0a897f2f 100644 --- a/src/main/java/org/qora/account/PrivateKeyAccount.java +++ b/src/main/java/org/qora/account/PrivateKeyAccount.java @@ -9,8 +9,6 @@ import org.qora.crypto.BouncyCastle25519; import org.qora.crypto.Crypto; import org.qora.repository.Repository; -import com.google.common.primitives.Bytes; - public class PrivateKeyAccount extends PublicKeyAccount { private static final int SIGNATURE_LENGTH = 64; @@ -70,8 +68,7 @@ public class PrivateKeyAccount extends PublicKeyAccount { public byte[] getProxyPrivateKey(byte[] publicKey) { byte[] sharedSecret = this.getSharedSecret(publicKey); - byte[] proxyHashData = Bytes.concat(sharedSecret, this.getPublicKey(), publicKey); - return Crypto.digest(proxyHashData); + return Crypto.digest(sharedSecret); } } diff --git a/src/main/java/org/qora/api/ApiError.java b/src/main/java/org/qora/api/ApiError.java index 49635754..b7702047 100644 --- a/src/main/java/org/qora/api/ApiError.java +++ b/src/main/java/org/qora/api/ApiError.java @@ -44,6 +44,7 @@ public enum ApiError { INVALID_REFERENCE(126, 400), TRANSFORMATION_ERROR(127, 400), INVALID_PRIVATE_KEY(128, 400), + INVALID_HEIGHT(129, 400), // WALLET WALLET_NO_EXISTS(201, 404), diff --git a/src/main/java/org/qora/api/model/ConnectedPeer.java b/src/main/java/org/qora/api/model/ConnectedPeer.java index c07482e3..9bd67db9 100644 --- a/src/main/java/org/qora/api/model/ConnectedPeer.java +++ b/src/main/java/org/qora/api/model/ConnectedPeer.java @@ -3,6 +3,7 @@ package org.qora.api.model; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import org.qora.network.Handshake; import org.qora.network.Peer; @XmlAccessorType(XmlAccessType.FIELD) @@ -16,9 +17,10 @@ public class ConnectedPeer { INBOUND, OUTBOUND; } - public Direction direction; + public Handshake handshakeStatus; + protected ConnectedPeer() { } @@ -27,6 +29,7 @@ public class ConnectedPeer { this.lastPing = peer.getLastPing(); this.direction = peer.isOutbound() ? Direction.OUTBOUND : Direction.INBOUND; this.lastHeight = peer.getPeerData() == null ? null : peer.getPeerData().getLastHeight(); + this.handshakeStatus = peer.getHandshakeStatus(); } } diff --git a/src/main/java/org/qora/api/resource/AdminResource.java b/src/main/java/org/qora/api/resource/AdminResource.java index 65e1c912..dd30cdf1 100644 --- a/src/main/java/org/qora/api/resource/AdminResource.java +++ b/src/main/java/org/qora/api/resource/AdminResource.java @@ -36,10 +36,12 @@ import org.qora.account.Forging; import org.qora.account.PrivateKeyAccount; import org.qora.api.ApiError; import org.qora.api.ApiErrors; +import org.qora.api.ApiException; import org.qora.api.ApiExceptionFactory; import org.qora.api.Security; import org.qora.api.model.ActivitySummary; import org.qora.api.model.NodeInfo; +import org.qora.block.BlockChain; import org.qora.controller.Controller; import org.qora.repository.DataException; import org.qora.repository.Repository; @@ -235,7 +237,7 @@ public class AdminResource { ) } ) - @ApiErrors({ApiError.REPOSITORY_ISSUE}) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE}) public String addForgingAccount(String seed58) { try (final Repository repository = RepositoryManager.getRepository()) { byte[] seed = Base58.decode(seed58.trim()); @@ -279,6 +281,7 @@ public class AdminResource { ) } ) + @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE}) public String deleteForgingAccount(String seed58) { try (final Repository repository = RepositoryManager.getRepository()) { byte[] seed = Base58.decode(seed58.trim()); @@ -355,4 +358,47 @@ public class AdminResource { } } + @POST + @Path("/orphan") + @Operation( + summary = "Discard blocks back to given height.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string", example = "0" + ) + ) + ), + responses = { + @ApiResponse( + description = "\"true\"", + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + @ApiErrors({ApiError.INVALID_HEIGHT, ApiError.REPOSITORY_ISSUE}) + public String orphan(String targetHeightString) { + Security.checkApiCallAllowed(request); + + try { + int targetHeight = Integer.parseUnsignedInt(targetHeightString); + + if (targetHeight <= 0 || targetHeight > Controller.getInstance().getChainHeight()) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT); + + if (BlockChain.orphan(targetHeight)) + return "true"; + else + return "false"; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } catch (NumberFormatException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_HEIGHT); + } catch (ApiException e) { + throw e; + } + } + } diff --git a/src/main/java/org/qora/block/BlockChain.java b/src/main/java/org/qora/block/BlockChain.java index 902531f8..648dc051 100644 --- a/src/main/java/org/qora/block/BlockChain.java +++ b/src/main/java/org/qora/block/BlockChain.java @@ -9,6 +9,8 @@ import java.math.MathContext; import java.sql.SQLException; import java.util.List; import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; @@ -24,6 +26,8 @@ import org.apache.logging.log4j.Logger; import org.eclipse.persistence.exceptions.XMLMarshalException; import org.eclipse.persistence.jaxb.JAXBContextFactory; import org.eclipse.persistence.jaxb.UnmarshallerProperties; +import org.qora.controller.Controller; +import org.qora.crypto.Crypto; import org.qora.data.block.BlockData; import org.qora.data.network.BlockSummaryData; import org.qora.network.Network; @@ -347,4 +351,27 @@ public class BlockChain { } } + public static boolean orphan(int targetHeight) throws DataException { + ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock(); + if (blockchainLock.tryLock()) + try { + try (final Repository repository = RepositoryManager.getRepository()) { + for (int height = repository.getBlockRepository().getBlockchainHeight(); height > targetHeight; --height) { + LOGGER.info(String.format("Forcably orphaning block %d", height)); + + BlockData blockData = repository.getBlockRepository().fromHeight(height); + Block block = new Block(repository, blockData); + block.orphan(); + repository.saveChanges(); + } + + return true; + } + } finally { + blockchainLock.unlock(); + } + + return false; + } + } diff --git a/src/main/java/org/qora/block/BlockGenerator.java b/src/main/java/org/qora/block/BlockGenerator.java index a9654cb3..c3e5a412 100644 --- a/src/main/java/org/qora/block/BlockGenerator.java +++ b/src/main/java/org/qora/block/BlockGenerator.java @@ -15,6 +15,7 @@ import org.qora.controller.Controller; import org.qora.data.account.ForgingAccountData; import org.qora.data.block.BlockData; import org.qora.data.transaction.TransactionData; +import org.qora.network.Network; import org.qora.repository.BlockRepository; import org.qora.repository.DataException; import org.qora.repository.Repository; @@ -82,6 +83,10 @@ public class BlockGenerator extends Thread { newBlocks.clear(); } + // Don't generate if we don't have enough connected peers as where would the transactions/consensus come from? + if (Network.getInstance().getUniqueHandshakedPeers().size() < Settings.getInstance().getMinBlockchainPeers()) + continue; + // Do we need to build any potential new blocks? List forgingAccountsData = repository.getAccountRepository().getForgingAccounts(); List forgingAccounts = forgingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getSeed())).collect(Collectors.toList()); diff --git a/src/main/java/org/qora/controller/Controller.java b/src/main/java/org/qora/controller/Controller.java index 2687649f..27fb5dc3 100644 --- a/src/main/java/org/qora/controller/Controller.java +++ b/src/main/java/org/qora/controller/Controller.java @@ -240,8 +240,8 @@ public class Controller extends Thread { return; // If we have enough peers, potentially synchronize - List peers = Network.getInstance().getHandshakeCompletedPeers(); - if (peers.size() < Settings.getInstance().getMinPeers()) + List peers = Network.getInstance().getUniqueHandshakedPeers(); + if (peers.size() < Settings.getInstance().getMinBlockchainPeers()) return; for(Peer peer : peers) diff --git a/src/main/java/org/qora/controller/Synchronizer.java b/src/main/java/org/qora/controller/Synchronizer.java index 6f5001b6..8574504b 100644 --- a/src/main/java/org/qora/controller/Synchronizer.java +++ b/src/main/java/org/qora/controller/Synchronizer.java @@ -105,8 +105,10 @@ public class Synchronizer { signatures.remove(0); // If common block is peer's latest block then we simply have a longer chain to peer, so exit now - if (commonBlockHeight == peerHeight) + if (commonBlockHeight == peerHeight) { + LOGGER.info(String.format("We have the same blockchain as peer %s, but longer", peer)); return SynchronizationResult.OK; + } // If common block is too far behind us then we're on massively different forks so give up. int minCommonHeight = ourHeight - MAXIMUM_COMMON_DELTA; diff --git a/src/main/java/org/qora/network/Handshake.java b/src/main/java/org/qora/network/Handshake.java index 2bf9da67..a0946156 100644 --- a/src/main/java/org/qora/network/Handshake.java +++ b/src/main/java/org/qora/network/Handshake.java @@ -8,7 +8,9 @@ import org.qora.controller.Controller; import org.qora.network.message.Message; import org.qora.network.message.Message.MessageType; import org.qora.network.message.PeerIdMessage; +import org.qora.network.message.PeerVerifyMessage; import org.qora.network.message.ProofMessage; +import org.qora.network.message.VerificationCodesMessage; import org.qora.network.message.VersionMessage; public enum Handshake { @@ -41,14 +43,30 @@ public enum Handshake { return null; } - // Set peer's ID - peer.setPeerId(peerId); - - // Is this ID already connected? We don't want both inbound and outbound so discard inbound if possible - Peer similarPeer = Network.getInstance().getOutboundPeerWithId(peerId); - if (similarPeer != null && similarPeer != peer) { - LOGGER.trace(String.format("Discarding inbound peer %s with existing ID", peer)); - return null; + // Is this ID already connected inbound or outbound? + Peer otherInboundPeer = Network.getInstance().getInboundPeerWithId(peerId); + + // Extra checks on inbound peers with known IDs, to prevent ID stealing + if (!peer.isOutbound() && otherInboundPeer != null) { + Peer otherOutboundPeer = Network.getInstance().getOutboundHandshakedPeerWithId(peerId); + + 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)); + 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 { + // Set peer's ID + peer.setPeerId(peerId); } return VERSION; @@ -117,6 +135,40 @@ 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(); + + // 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); @@ -160,4 +212,20 @@ public enum Handshake { } } + 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(); // give up with this peer instead + return; + } + + // Send PEER_VERIFY to peer + Message peerVerifyMessage = new PeerVerifyMessage(peer.getVerificationCodeSent()); + if (!peer.sendMessage(peerVerifyMessage)) + peer.disconnect(); + } + } diff --git a/src/main/java/org/qora/network/Network.java b/src/main/java/org/qora/network/Network.java index 2d2b5846..e8b51b92 100644 --- a/src/main/java/org/qora/network/Network.java +++ b/src/main/java/org/qora/network/Network.java @@ -31,10 +31,12 @@ import org.qora.network.message.GetPeersMessage; import org.qora.network.message.HeightMessage; import org.qora.network.message.Message; import org.qora.network.message.Message.MessageType; +import org.qora.network.message.PeerVerifyMessage; import org.qora.network.message.PeersMessage; import org.qora.network.message.PeersV2Message; import org.qora.network.message.PingMessage; import org.qora.network.message.TransactionMessage; +import org.qora.network.message.VerificationCodesMessage; import org.qora.repository.DataException; import org.qora.repository.Repository; import org.qora.repository.RepositoryManager; @@ -69,7 +71,7 @@ public class Network extends Thread { private List connectedPeers; private List selfPeers; private ServerSocket listenSocket; - private int minPeers; + private int minOutboundPeers; private int maxPeers; private ExecutorService peerExecutor; private ExecutorService mergePeersExecutor; @@ -106,7 +108,7 @@ public class Network extends Thread { ourPeerId = new byte[PEER_ID_LENGTH]; new SecureRandom().nextBytes(ourPeerId); - minPeers = Settings.getInstance().getMinPeers(); + minOutboundPeers = Settings.getInstance().getMinOutboundPeers(); maxPeers = Settings.getInstance().getMaxPeers(); peerExecutor = Executors.newCachedThreadPool(); @@ -269,7 +271,7 @@ public class Network extends Thread { } private void createConnection() throws InterruptedException, DataException { - if (this.getOutboundHandshakeCompletedPeers().size() >= minPeers) + if (this.getOutboundHandshakedPeers().size() >= minOutboundPeers) return; Peer newPeer; @@ -389,6 +391,21 @@ public class Network extends Thread { // Should be non-handshaking messages from now on switch (message.getType()) { + case PEER_VERIFY: + // Remote peer wants extra verification + possibleVerificationResponse(peer); + break; + + case VERIFICATION_CODES: + 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); + break; + case VERSION: case PEER_ID: case PROOF: @@ -447,7 +464,30 @@ public class Network extends Thread { } } + 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(); + 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 dopplegangers? + if (peer.getPendingPeerId() != null) { + peer.setHandshakeStatus(Handshake.PEER_VERIFY); + peer.getHandshakeStatus().action(peer); + return; + } + // Make a note that we've successfully completed handshake (and when) peer.getPeerData().setLastConnected(NTP.getTime()); @@ -551,7 +591,7 @@ public class Network extends Thread { // Network-wide calls /** Returns list of connected peers that have completed handshaking. */ - public List getHandshakeCompletedPeers() { + public List getHandshakedPeers() { List peers = new ArrayList<>(); synchronized (this.connectedPeers) { @@ -561,8 +601,31 @@ public class Network extends Thread { return peers; } + /** Returns list of connected peers that have completed handshaking, with unbound duplicates removed. */ + public List getUniqueHandshakedPeers() { + final List peers; + + synchronized (this.connectedPeers) { + peers = this.connectedPeers.stream().filter(peer -> peer.getHandshakeStatus() == Handshake.COMPLETED).collect(Collectors.toList()); + } + + // Returns true if this [inbound] peer 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 getOutboundHandshakeCompletedPeers() { + public List getOutboundHandshakedPeers() { List peers = new ArrayList<>(); synchronized (this.connectedPeers) { @@ -573,10 +636,17 @@ public class Network extends Thread { return peers; } - /** Returns Peer with outbound connection and passed ID, or null if none found. */ - public Peer getOutboundPeerWithId(byte[] peerId) { + /** Returns Peer with inbound connection and matching ID, or null if none found. */ + public Peer getInboundPeerWithId(byte[] peerId) { + synchronized (this.connectedPeers) { + return this.connectedPeers.stream().filter(peer -> !peer.isOutbound() && peer.getPeerId() != null && Arrays.equals(peer.getPeerId(), peerId)).findAny().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.getPeerId() != null && Arrays.equals(peer.getPeerId(), peerId)).findAny().orElse(null); + return this.connectedPeers.stream().filter(peer -> peer.isOutbound() && peer.getHandshakeStatus() == Handshake.COMPLETED && peer.getPeerId() != null && Arrays.equals(peer.getPeerId(), peerId)).findAny().orElse(null); } } @@ -647,7 +717,7 @@ public class Network extends Thread { } try { - peerExecutor.execute(new Broadcaster(this.getHandshakeCompletedPeers(), peerMessage)); + peerExecutor.execute(new Broadcaster(this.getUniqueHandshakedPeers(), peerMessage)); } catch (RejectedExecutionException e) { // Can't execute - probably because we're shutting down, so ignore } diff --git a/src/main/java/org/qora/network/Peer.java b/src/main/java/org/qora/network/Peer.java index 834c7a88..faa4dc22 100644 --- a/src/main/java/org/qora/network/Peer.java +++ b/src/main/java/org/qora/network/Peer.java @@ -56,6 +56,10 @@ public class Peer implements Runnable { private boolean isLocal; private byte[] peerId; + private byte[] pendingPeerId; + private byte[] verificationCodeSent; + private byte[] verificationCodeExpected; + /** Construct unconnected outbound Peer using socket address in peer data */ public Peer(PeerData peerData) { this.isOutbound = true; @@ -133,6 +137,27 @@ public class Peer implements Runnable { this.peerId = peerId; } + public byte[] getPendingPeerId() { + return this.pendingPeerId; + } + + public void setPendingPeerId(byte[] peerId) { + this.pendingPeerId = peerId; + } + + 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; + } + // Easier, and nicer output, than peer.getRemoteSocketAddress() @Override @@ -142,6 +167,14 @@ public class Peer implements Runnable { // 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 setup() throws IOException { this.socket.setSoTimeout(INACTIVITY_TIMEOUT); this.out = this.socket.getOutputStream(); diff --git a/src/main/java/org/qora/network/message/Message.java b/src/main/java/org/qora/network/message/Message.java index 8859f1d9..14679e93 100644 --- a/src/main/java/org/qora/network/message/Message.java +++ b/src/main/java/org/qora/network/message/Message.java @@ -43,7 +43,9 @@ public abstract class Message { PEERS_V2(13), GET_BLOCK_SUMMARIES(14), BLOCK_SUMMARIES(15), - GET_SIGNATURES_V2(16); + GET_SIGNATURES_V2(16), + PEER_VERIFY(17), + VERIFICATION_CODES(18); public final int value; public final Method fromByteBuffer; diff --git a/src/main/java/org/qora/network/message/PeerVerifyMessage.java b/src/main/java/org/qora/network/message/PeerVerifyMessage.java new file mode 100644 index 00000000..84d39454 --- /dev/null +++ b/src/main/java/org/qora/network/message/PeerVerifyMessage.java @@ -0,0 +1,51 @@ +package org.qora.network.message; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +import org.qora.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/qora/network/message/VerificationCodesMessage.java b/src/main/java/org/qora/network/message/VerificationCodesMessage.java new file mode 100644 index 00000000..f09f165d --- /dev/null +++ b/src/main/java/org/qora/network/message/VerificationCodesMessage.java @@ -0,0 +1,64 @@ +package org.qora.network.message; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +import org.qora.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/qora/orphan.java b/src/main/java/org/qora/orphan.java index be5a1da0..38fcd649 100644 --- a/src/main/java/org/qora/orphan.java +++ b/src/main/java/org/qora/orphan.java @@ -2,12 +2,9 @@ package org.qora; import java.security.Security; import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.qora.block.Block; import org.qora.block.BlockChain; import org.qora.controller.Controller; -import org.qora.data.block.BlockData; import org.qora.repository.DataException; -import org.qora.repository.Repository; import org.qora.repository.RepositoryFactory; import org.qora.repository.RepositoryManager; import org.qora.repository.hsqldb.HSQLDBRepositoryFactory; @@ -43,15 +40,8 @@ public class orphan { System.exit(2); } - try (final Repository repository = RepositoryManager.getRepository()) { - for (int height = repository.getBlockRepository().getBlockchainHeight(); height > targetHeight; --height) { - System.out.println("Orphaning block " + height); - - BlockData blockData = repository.getBlockRepository().fromHeight(height); - Block block = new Block(repository, blockData); - block.orphan(); - repository.saveChanges(); - } + try { + BlockChain.orphan(targetHeight); } catch (DataException e) { e.printStackTrace(); } diff --git a/src/main/java/org/qora/settings/Settings.java b/src/main/java/org/qora/settings/Settings.java index 27721cdc..cc94caf9 100644 --- a/src/main/java/org/qora/settings/Settings.java +++ b/src/main/java/org/qora/settings/Settings.java @@ -57,7 +57,11 @@ public class Settings { // Peer-to-peer related private int listenPort = DEFAULT_LISTEN_PORT; private String bindAddress = null; // listen on all local addresses - private int minPeers = 3; + /** Minimum number of peers to allow block generation / synchronization. */ + private int minBlockchainPeers = 3; + /** Target number of outbound connections to peers we should make. */ + private int minOutboundPeers = 3; + /** Maximum number of peer connections we allow. */ private int maxPeers = 10; // Which blockchains this node is running @@ -223,8 +227,12 @@ public class Settings { return this.bindAddress; } - public int getMinPeers() { - return this.minPeers; + public int getMinBlockchainPeers() { + return this.minBlockchainPeers; + } + + public int getMinOutboundPeers() { + return this.minOutboundPeers; } public int getMaxPeers() { diff --git a/src/main/resources/i18n/ApiError_en.properties b/src/main/resources/i18n/ApiError_en.properties index 91f39c0a..7b5bd8c4 100644 --- a/src/main/resources/i18n/ApiError_en.properties +++ b/src/main/resources/i18n/ApiError_en.properties @@ -36,6 +36,7 @@ ADDRESS_NO_EXISTS=account address does not exist INVALID_CRITERIA=invalid search criteria INVALID_REFERENCE=invalid reference INVALID_PRIVATE_KEY=invalid private key +INVALID_HEIGHT=invalid block height # Wallet WALLET_NO_EXISTS=wallet does not exist diff --git a/src/test/java/org/qora/test/GuiTests.java b/src/test/java/org/qora/test/GuiTests.java new file mode 100644 index 00000000..6b2cb35b --- /dev/null +++ b/src/test/java/org/qora/test/GuiTests.java @@ -0,0 +1,17 @@ +package org.qora.test; + +import org.junit.Test; +import org.qora.gui.SplashFrame; + +public class GuiTests { + + @Test + public void testSplashFrame() throws InterruptedException { + SplashFrame splashFrame = SplashFrame.getInstance(); + + Thread.sleep(2000L); + + splashFrame.dispose(); + } + +} diff --git a/src/test/resources/proxy-key-example.html b/src/test/resources/proxy-key-example.html index 900167a5..7b679a2e 100644 --- a/src/test/resources/proxy-key-example.html +++ b/src/test/resources/proxy-key-example.html @@ -25,18 +25,8 @@ var sharedSecret = nacl.crypto_scalarmult(mintingX25519Prk, recipientAccountX25519Puk); console.log("shared secret (for debugging): " + Base58.encode(sharedSecret)); - // Data to be hashed: shared secret (32 bytes) + minting public key (32 bytes) + recipient public key (32 bytes) - // or, in general terms: shared secret (32 bytes) + public key from private key (32 bytes) + other party's public key (32 bytes) - var proxyHashData = new Uint8Array(sharedSecret.length + mintingAccountPuk.length + recipientAccountPuk.length); - // copy shared secret into array, starting at index 0 - proxyHashData.set(sharedSecret); - // copy minting account public key into array, starting at index 32 - proxyHashData.set(mintingAccountPuk, sharedSecret.length); - // copy recipient account public key into array, starting at index 64 (32 + 32) - proxyHashData.set(recipientAccountPuk, sharedSecret.length + mintingAccountPuk.length); - - // Proxy PRIVATE key is SHA256 of data above - var proxyPrivateKey = nacl.crypto_hash_sha256(proxyHashData) + // Proxy PRIVATE key is SHA256 of shared secret + var proxyPrivateKey = nacl.crypto_hash_sha256(sharedSecret) console.log("proxy private key: " + Base58.encode(proxyPrivateKey)); var proxyKeyPair = nacl.crypto_sign_seed_keypair(proxyPrivateKey);