Browse Source

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.
split-DB
catbref 5 years ago
parent
commit
8f7c954f5a
  1. 5
      src/main/java/org/qora/account/PrivateKeyAccount.java
  2. 1
      src/main/java/org/qora/api/ApiError.java
  3. 5
      src/main/java/org/qora/api/model/ConnectedPeer.java
  4. 48
      src/main/java/org/qora/api/resource/AdminResource.java
  5. 27
      src/main/java/org/qora/block/BlockChain.java
  6. 5
      src/main/java/org/qora/block/BlockGenerator.java
  7. 4
      src/main/java/org/qora/controller/Controller.java
  8. 4
      src/main/java/org/qora/controller/Synchronizer.java
  9. 84
      src/main/java/org/qora/network/Handshake.java
  10. 88
      src/main/java/org/qora/network/Network.java
  11. 33
      src/main/java/org/qora/network/Peer.java
  12. 4
      src/main/java/org/qora/network/message/Message.java
  13. 51
      src/main/java/org/qora/network/message/PeerVerifyMessage.java
  14. 64
      src/main/java/org/qora/network/message/VerificationCodesMessage.java
  15. 14
      src/main/java/org/qora/orphan.java
  16. 14
      src/main/java/org/qora/settings/Settings.java
  17. 1
      src/main/resources/i18n/ApiError_en.properties
  18. 17
      src/test/java/org/qora/test/GuiTests.java
  19. 14
      src/test/resources/proxy-key-example.html

5
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.crypto.Crypto;
import org.qora.repository.Repository; import org.qora.repository.Repository;
import com.google.common.primitives.Bytes;
public class PrivateKeyAccount extends PublicKeyAccount { public class PrivateKeyAccount extends PublicKeyAccount {
private static final int SIGNATURE_LENGTH = 64; private static final int SIGNATURE_LENGTH = 64;
@ -70,8 +68,7 @@ public class PrivateKeyAccount extends PublicKeyAccount {
public byte[] getProxyPrivateKey(byte[] publicKey) { public byte[] getProxyPrivateKey(byte[] publicKey) {
byte[] sharedSecret = this.getSharedSecret(publicKey); byte[] sharedSecret = this.getSharedSecret(publicKey);
byte[] proxyHashData = Bytes.concat(sharedSecret, this.getPublicKey(), publicKey); return Crypto.digest(sharedSecret);
return Crypto.digest(proxyHashData);
} }
} }

1
src/main/java/org/qora/api/ApiError.java

@ -44,6 +44,7 @@ public enum ApiError {
INVALID_REFERENCE(126, 400), INVALID_REFERENCE(126, 400),
TRANSFORMATION_ERROR(127, 400), TRANSFORMATION_ERROR(127, 400),
INVALID_PRIVATE_KEY(128, 400), INVALID_PRIVATE_KEY(128, 400),
INVALID_HEIGHT(129, 400),
// WALLET // WALLET
WALLET_NO_EXISTS(201, 404), WALLET_NO_EXISTS(201, 404),

5
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.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlAccessorType;
import org.qora.network.Handshake;
import org.qora.network.Peer; import org.qora.network.Peer;
@XmlAccessorType(XmlAccessType.FIELD) @XmlAccessorType(XmlAccessType.FIELD)
@ -16,9 +17,10 @@ public class ConnectedPeer {
INBOUND, INBOUND,
OUTBOUND; OUTBOUND;
} }
public Direction direction; public Direction direction;
public Handshake handshakeStatus;
protected ConnectedPeer() { protected ConnectedPeer() {
} }
@ -27,6 +29,7 @@ public class ConnectedPeer {
this.lastPing = peer.getLastPing(); this.lastPing = peer.getLastPing();
this.direction = peer.isOutbound() ? Direction.OUTBOUND : Direction.INBOUND; this.direction = peer.isOutbound() ? Direction.OUTBOUND : Direction.INBOUND;
this.lastHeight = peer.getPeerData() == null ? null : peer.getPeerData().getLastHeight(); this.lastHeight = peer.getPeerData() == null ? null : peer.getPeerData().getLastHeight();
this.handshakeStatus = peer.getHandshakeStatus();
} }
} }

48
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.account.PrivateKeyAccount;
import org.qora.api.ApiError; import org.qora.api.ApiError;
import org.qora.api.ApiErrors; import org.qora.api.ApiErrors;
import org.qora.api.ApiException;
import org.qora.api.ApiExceptionFactory; import org.qora.api.ApiExceptionFactory;
import org.qora.api.Security; import org.qora.api.Security;
import org.qora.api.model.ActivitySummary; import org.qora.api.model.ActivitySummary;
import org.qora.api.model.NodeInfo; import org.qora.api.model.NodeInfo;
import org.qora.block.BlockChain;
import org.qora.controller.Controller; import org.qora.controller.Controller;
import org.qora.repository.DataException; import org.qora.repository.DataException;
import org.qora.repository.Repository; 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) { public String addForgingAccount(String seed58) {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
byte[] seed = Base58.decode(seed58.trim()); 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) { public String deleteForgingAccount(String seed58) {
try (final Repository repository = RepositoryManager.getRepository()) { try (final Repository repository = RepositoryManager.getRepository()) {
byte[] seed = Base58.decode(seed58.trim()); 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;
}
}
} }

27
src/main/java/org/qora/block/BlockChain.java

@ -9,6 +9,8 @@ import java.math.MathContext;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException; 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.exceptions.XMLMarshalException;
import org.eclipse.persistence.jaxb.JAXBContextFactory; import org.eclipse.persistence.jaxb.JAXBContextFactory;
import org.eclipse.persistence.jaxb.UnmarshallerProperties; 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.block.BlockData;
import org.qora.data.network.BlockSummaryData; import org.qora.data.network.BlockSummaryData;
import org.qora.network.Network; 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;
}
} }

5
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.account.ForgingAccountData;
import org.qora.data.block.BlockData; import org.qora.data.block.BlockData;
import org.qora.data.transaction.TransactionData; import org.qora.data.transaction.TransactionData;
import org.qora.network.Network;
import org.qora.repository.BlockRepository; import org.qora.repository.BlockRepository;
import org.qora.repository.DataException; import org.qora.repository.DataException;
import org.qora.repository.Repository; import org.qora.repository.Repository;
@ -82,6 +83,10 @@ public class BlockGenerator extends Thread {
newBlocks.clear(); 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? // Do we need to build any potential new blocks?
List<ForgingAccountData> forgingAccountsData = repository.getAccountRepository().getForgingAccounts(); List<ForgingAccountData> forgingAccountsData = repository.getAccountRepository().getForgingAccounts();
List<PrivateKeyAccount> forgingAccounts = forgingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getSeed())).collect(Collectors.toList()); List<PrivateKeyAccount> forgingAccounts = forgingAccountsData.stream().map(accountData -> new PrivateKeyAccount(repository, accountData.getSeed())).collect(Collectors.toList());

4
src/main/java/org/qora/controller/Controller.java

@ -240,8 +240,8 @@ public class Controller extends Thread {
return; return;
// If we have enough peers, potentially synchronize // If we have enough peers, potentially synchronize
List<Peer> peers = Network.getInstance().getHandshakeCompletedPeers(); List<Peer> peers = Network.getInstance().getUniqueHandshakedPeers();
if (peers.size() < Settings.getInstance().getMinPeers()) if (peers.size() < Settings.getInstance().getMinBlockchainPeers())
return; return;
for(Peer peer : peers) for(Peer peer : peers)

4
src/main/java/org/qora/controller/Synchronizer.java

@ -105,8 +105,10 @@ public class Synchronizer {
signatures.remove(0); signatures.remove(0);
// If common block is peer's latest block then we simply have a longer chain to peer, so exit now // 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; return SynchronizationResult.OK;
}
// If common block is too far behind us then we're on massively different forks so give up. // If common block is too far behind us then we're on massively different forks so give up.
int minCommonHeight = ourHeight - MAXIMUM_COMMON_DELTA; int minCommonHeight = ourHeight - MAXIMUM_COMMON_DELTA;

84
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;
import org.qora.network.message.Message.MessageType; import org.qora.network.message.Message.MessageType;
import org.qora.network.message.PeerIdMessage; import org.qora.network.message.PeerIdMessage;
import org.qora.network.message.PeerVerifyMessage;
import org.qora.network.message.ProofMessage; import org.qora.network.message.ProofMessage;
import org.qora.network.message.VerificationCodesMessage;
import org.qora.network.message.VersionMessage; import org.qora.network.message.VersionMessage;
public enum Handshake { public enum Handshake {
@ -41,14 +43,30 @@ public enum Handshake {
return null; return null;
} }
// Set peer's ID // Is this ID already connected inbound or outbound?
peer.setPeerId(peerId); Peer otherInboundPeer = Network.getInstance().getInboundPeerWithId(peerId);
// Is this ID already connected? We don't want both inbound and outbound so discard inbound if possible // Extra checks on inbound peers with known IDs, to prevent ID stealing
Peer similarPeer = Network.getInstance().getOutboundPeerWithId(peerId); if (!peer.isOutbound() && otherInboundPeer != null) {
if (similarPeer != null && similarPeer != peer) { Peer otherOutboundPeer = Network.getInstance().getOutboundHandshakedPeerWithId(peerId);
LOGGER.trace(String.format("Discarding inbound peer %s with existing ID", peer));
return 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));
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; return VERSION;
@ -117,6 +135,40 @@ public enum Handshake {
public void action(Peer peer) { public void action(Peer peer) {
// Note: this is only called when we've made outbound connection // 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); 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();
}
} }

88
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.HeightMessage;
import org.qora.network.message.Message; import org.qora.network.message.Message;
import org.qora.network.message.Message.MessageType; import org.qora.network.message.Message.MessageType;
import org.qora.network.message.PeerVerifyMessage;
import org.qora.network.message.PeersMessage; import org.qora.network.message.PeersMessage;
import org.qora.network.message.PeersV2Message; import org.qora.network.message.PeersV2Message;
import org.qora.network.message.PingMessage; import org.qora.network.message.PingMessage;
import org.qora.network.message.TransactionMessage; import org.qora.network.message.TransactionMessage;
import org.qora.network.message.VerificationCodesMessage;
import org.qora.repository.DataException; import org.qora.repository.DataException;
import org.qora.repository.Repository; import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager; import org.qora.repository.RepositoryManager;
@ -69,7 +71,7 @@ public class Network extends Thread {
private List<Peer> connectedPeers; private List<Peer> connectedPeers;
private List<PeerAddress> selfPeers; private List<PeerAddress> selfPeers;
private ServerSocket listenSocket; private ServerSocket listenSocket;
private int minPeers; private int minOutboundPeers;
private int maxPeers; private int maxPeers;
private ExecutorService peerExecutor; private ExecutorService peerExecutor;
private ExecutorService mergePeersExecutor; private ExecutorService mergePeersExecutor;
@ -106,7 +108,7 @@ public class Network extends Thread {
ourPeerId = new byte[PEER_ID_LENGTH]; ourPeerId = new byte[PEER_ID_LENGTH];
new SecureRandom().nextBytes(ourPeerId); new SecureRandom().nextBytes(ourPeerId);
minPeers = Settings.getInstance().getMinPeers(); minOutboundPeers = Settings.getInstance().getMinOutboundPeers();
maxPeers = Settings.getInstance().getMaxPeers(); maxPeers = Settings.getInstance().getMaxPeers();
peerExecutor = Executors.newCachedThreadPool(); peerExecutor = Executors.newCachedThreadPool();
@ -269,7 +271,7 @@ public class Network extends Thread {
} }
private void createConnection() throws InterruptedException, DataException { private void createConnection() throws InterruptedException, DataException {
if (this.getOutboundHandshakeCompletedPeers().size() >= minPeers) if (this.getOutboundHandshakedPeers().size() >= minOutboundPeers)
return; return;
Peer newPeer; Peer newPeer;
@ -389,6 +391,21 @@ public class Network extends Thread {
// Should be non-handshaking messages from now on // Should be non-handshaking messages from now on
switch (message.getType()) { 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 VERSION:
case PEER_ID: case PEER_ID:
case PROOF: 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) { 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) // Make a note that we've successfully completed handshake (and when)
peer.getPeerData().setLastConnected(NTP.getTime()); peer.getPeerData().setLastConnected(NTP.getTime());
@ -551,7 +591,7 @@ public class Network extends Thread {
// Network-wide calls // Network-wide calls
/** Returns list of connected peers that have completed handshaking. */ /** Returns list of connected peers that have completed handshaking. */
public List<Peer> getHandshakeCompletedPeers() { public List<Peer> getHandshakedPeers() {
List<Peer> peers = new ArrayList<>(); List<Peer> peers = new ArrayList<>();
synchronized (this.connectedPeers) { synchronized (this.connectedPeers) {
@ -561,8 +601,31 @@ public class Network extends Thread {
return peers; return peers;
} }
/** Returns list of connected peers that have completed handshaking, with unbound duplicates removed. */
public List<Peer> getUniqueHandshakedPeers() {
final List<Peer> 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<Peer> 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. */ /** Returns list of peers we connected to that have completed handshaking. */
public List<Peer> getOutboundHandshakeCompletedPeers() { public List<Peer> getOutboundHandshakedPeers() {
List<Peer> peers = new ArrayList<>(); List<Peer> peers = new ArrayList<>();
synchronized (this.connectedPeers) { synchronized (this.connectedPeers) {
@ -573,10 +636,17 @@ public class Network extends Thread {
return peers; return peers;
} }
/** Returns Peer with outbound connection and passed ID, or null if none found. */ /** Returns Peer with inbound connection and matching ID, or null if none found. */
public Peer getOutboundPeerWithId(byte[] peerId) { 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) { 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 { try {
peerExecutor.execute(new Broadcaster(this.getHandshakeCompletedPeers(), peerMessage)); peerExecutor.execute(new Broadcaster(this.getUniqueHandshakedPeers(), peerMessage));
} catch (RejectedExecutionException e) { } catch (RejectedExecutionException e) {
// Can't execute - probably because we're shutting down, so ignore // Can't execute - probably because we're shutting down, so ignore
} }

33
src/main/java/org/qora/network/Peer.java

@ -56,6 +56,10 @@ public class Peer implements Runnable {
private boolean isLocal; private boolean isLocal;
private byte[] peerId; private byte[] peerId;
private byte[] pendingPeerId;
private byte[] verificationCodeSent;
private byte[] verificationCodeExpected;
/** Construct unconnected outbound Peer using socket address in peer data */ /** Construct unconnected outbound Peer using socket address in peer data */
public Peer(PeerData peerData) { public Peer(PeerData peerData) {
this.isOutbound = true; this.isOutbound = true;
@ -133,6 +137,27 @@ public class Peer implements Runnable {
this.peerId = peerId; 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() // Easier, and nicer output, than peer.getRemoteSocketAddress()
@Override @Override
@ -142,6 +167,14 @@ public class Peer implements Runnable {
// Processing // 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 { private void setup() throws IOException {
this.socket.setSoTimeout(INACTIVITY_TIMEOUT); this.socket.setSoTimeout(INACTIVITY_TIMEOUT);
this.out = this.socket.getOutputStream(); this.out = this.socket.getOutputStream();

4
src/main/java/org/qora/network/message/Message.java

@ -43,7 +43,9 @@ public abstract class Message {
PEERS_V2(13), PEERS_V2(13),
GET_BLOCK_SUMMARIES(14), GET_BLOCK_SUMMARIES(14),
BLOCK_SUMMARIES(15), BLOCK_SUMMARIES(15),
GET_SIGNATURES_V2(16); GET_SIGNATURES_V2(16),
PEER_VERIFY(17),
VERIFICATION_CODES(18);
public final int value; public final int value;
public final Method fromByteBuffer; public final Method fromByteBuffer;

51
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;
}
}
}

64
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;
}
}
}

14
src/main/java/org/qora/orphan.java

@ -2,12 +2,9 @@ package org.qora;
import java.security.Security; import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qora.block.Block;
import org.qora.block.BlockChain; import org.qora.block.BlockChain;
import org.qora.controller.Controller; import org.qora.controller.Controller;
import org.qora.data.block.BlockData;
import org.qora.repository.DataException; import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryFactory; import org.qora.repository.RepositoryFactory;
import org.qora.repository.RepositoryManager; import org.qora.repository.RepositoryManager;
import org.qora.repository.hsqldb.HSQLDBRepositoryFactory; import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
@ -43,15 +40,8 @@ public class orphan {
System.exit(2); System.exit(2);
} }
try (final Repository repository = RepositoryManager.getRepository()) { try {
for (int height = repository.getBlockRepository().getBlockchainHeight(); height > targetHeight; --height) { BlockChain.orphan(targetHeight);
System.out.println("Orphaning block " + height);
BlockData blockData = repository.getBlockRepository().fromHeight(height);
Block block = new Block(repository, blockData);
block.orphan();
repository.saveChanges();
}
} catch (DataException e) { } catch (DataException e) {
e.printStackTrace(); e.printStackTrace();
} }

14
src/main/java/org/qora/settings/Settings.java

@ -57,7 +57,11 @@ public class Settings {
// Peer-to-peer related // Peer-to-peer related
private int listenPort = DEFAULT_LISTEN_PORT; private int listenPort = DEFAULT_LISTEN_PORT;
private String bindAddress = null; // listen on all local addresses 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; private int maxPeers = 10;
// Which blockchains this node is running // Which blockchains this node is running
@ -223,8 +227,12 @@ public class Settings {
return this.bindAddress; return this.bindAddress;
} }
public int getMinPeers() { public int getMinBlockchainPeers() {
return this.minPeers; return this.minBlockchainPeers;
}
public int getMinOutboundPeers() {
return this.minOutboundPeers;
} }
public int getMaxPeers() { public int getMaxPeers() {

1
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_CRITERIA=invalid search criteria
INVALID_REFERENCE=invalid reference INVALID_REFERENCE=invalid reference
INVALID_PRIVATE_KEY=invalid private key INVALID_PRIVATE_KEY=invalid private key
INVALID_HEIGHT=invalid block height
# Wallet # Wallet
WALLET_NO_EXISTS=wallet does not exist WALLET_NO_EXISTS=wallet does not exist

17
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();
}
}

14
src/test/resources/proxy-key-example.html

@ -25,18 +25,8 @@
var sharedSecret = nacl.crypto_scalarmult(mintingX25519Prk, recipientAccountX25519Puk); var sharedSecret = nacl.crypto_scalarmult(mintingX25519Prk, recipientAccountX25519Puk);
console.log("shared secret (for debugging): " + Base58.encode(sharedSecret)); 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) // Proxy PRIVATE key is SHA256 of shared secret
// or, in general terms: shared secret (32 bytes) + public key from private key (32 bytes) + other party's public key (32 bytes) var proxyPrivateKey = nacl.crypto_hash_sha256(sharedSecret)
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)
console.log("proxy private key: " + Base58.encode(proxyPrivateKey)); console.log("proxy private key: " + Base58.encode(proxyPrivateKey));
var proxyKeyPair = nacl.crypto_sign_seed_keypair(proxyPrivateKey); var proxyKeyPair = nacl.crypto_sign_seed_keypair(proxyPrivateKey);

Loading…
Cancel
Save