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 inbound or outbound? + Peer otherInboundPeer = Network.getInstance().getInboundPeerWithId(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; + // 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); + 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.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);