Merge pull request #238 from AlphaX-Qortal/master

Added real address to API results - Currently the address shown in the API results when querying blocks, shows an address formed by the 'reward share public key', this address is not useful for viewing, as it is not the address utilized for QORT. This change makes it so the 'real' Qortal address is displayed instead of this useless address. Thanks @AlphaX-Qortal

Added group member check to validations - validation fixes.

Network changes - Moved unnecessary 'we already have connection' messages from info logging to debug. Updated minPeerVersion default to current release version. (4.6.5). Updated default peer list. Updated syntax. Updated formatting.

Updated dependencies

Thanks @AlphaX-Qortal
This commit is contained in:
crowetic 2024-12-02 14:42:34 -08:00 committed by GitHub
commit a8d73926b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 182 additions and 84 deletions

View File

@ -22,7 +22,7 @@
<dagger.version>1.2.2</dagger.version>
<extendedset.version>0.12.3</extendedset.version>
<git-commit-id-plugin.version>4.9.10</git-commit-id-plugin.version>
<grpc.version>1.68.1</grpc.version>
<grpc.version>1.68.2</grpc.version>
<guava.version>33.3.1-jre</guava.version>
<hamcrest-library.version>2.2</hamcrest-library.version>
<homoglyph.version>1.2.1</homoglyph.version>
@ -35,7 +35,7 @@
<jetty.version>9.4.56.v20240826</jetty.version>
<json-simple.version>1.1.1</json-simple.version>
<json.version>20240303</json.version>
<jsoup.version>1.18.1</jsoup.version>
<jsoup.version>1.18.3</jsoup.version>
<junit-jupiter-engine.version>5.11.0-M2</junit-jupiter-engine.version>
<lifecycle-mapping.version>1.0.0</lifecycle-mapping.version>
<log4j.version>2.23.1</log4j.version>

View File

@ -348,6 +348,24 @@ public class Account {
return accountData.getLevel();
}
/**
* Returns reward-share minting address, or unknown if reward-share does not exist.
*
* @param repository
* @param rewardSharePublicKey
* @return address or unknown
* @throws DataException
*/
public static String getRewardShareMintingAddress(Repository repository, byte[] rewardSharePublicKey) throws DataException {
// Find actual minter address
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(rewardSharePublicKey);
if (rewardShareData == null)
return "Unknown";
return rewardShareData.getMinter();
}
/**
* Returns 'effective' minting level, or zero if reward-share does not exist.
*

View File

@ -1,7 +1,13 @@
package org.qortal.api.model;
import org.qortal.account.Account;
import org.qortal.repository.DataException;
import org.qortal.repository.RepositoryManager;
import org.qortal.repository.Repository;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@ -47,4 +53,31 @@ public class ApiOnlineAccount {
return this.recipientAddress;
}
public int getMinterLevelFromPublicKey() {
try (final Repository repository = RepositoryManager.getRepository()) {
return Account.getRewardShareEffectiveMintingLevel(repository, this.rewardSharePublicKey);
} catch (DataException e) {
return 0;
}
}
public boolean getIsMember() {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getGroupRepository().memberExists(694, getMinterAddress());
} catch (DataException e) {
return false;
}
}
// JAXB special
@XmlElement(name = "minterLevel")
protected int getMinterLevel() {
return getMinterLevelFromPublicKey();
}
@XmlElement(name = "isMinterMember")
protected boolean getMinterMember() {
return getIsMember();
}
}

View File

@ -9,6 +9,7 @@ import java.math.BigInteger;
public class BlockMintingInfo {
public byte[] minterPublicKey;
public String minterAddress;
public int minterLevel;
public int onlineAccountsCount;
public BigDecimal maxDistance;
@ -19,5 +20,4 @@ public class BlockMintingInfo {
public BlockMintingInfo() {
}
}

View File

@ -542,6 +542,7 @@ public class BlocksResource {
}
}
String minterAddress = Account.getRewardShareMintingAddress(repository, blockData.getMinterPublicKey());
int minterLevel = Account.getRewardShareEffectiveMintingLevel(repository, blockData.getMinterPublicKey());
if (minterLevel == 0)
// This may be unavailable when requesting a trimmed block
@ -554,6 +555,7 @@ public class BlocksResource {
BlockMintingInfo blockMintingInfo = new BlockMintingInfo();
blockMintingInfo.minterPublicKey = blockData.getMinterPublicKey();
blockMintingInfo.minterAddress = minterAddress;
blockMintingInfo.minterLevel = minterLevel;
blockMintingInfo.onlineAccountsCount = blockData.getOnlineAccountsCount();
blockMintingInfo.maxDistance = new BigDecimal(block.MAX_DISTANCE);
@ -887,5 +889,4 @@ public class BlocksResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@ -414,6 +414,21 @@ public class Block {
});
}
// After feature trigger, remove any online accounts that are not minter group member
if (height >= BlockChain.getInstance().getGroupMemberCheckHeight()) {
onlineAccounts.removeIf(a -> {
try {
int groupId = BlockChain.getInstance().getMintingGroupId();
String address = Account.getRewardShareMintingAddress(repository, a.getPublicKey());
boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address);
return !isMinterGroupMember;
} catch (DataException e) {
// Something went wrong, so remove the account
return true;
}
});
}
if (onlineAccounts.isEmpty()) {
LOGGER.debug("No online accounts - not even our own?");
return null;
@ -721,19 +736,19 @@ public class Block {
List<ExpandedAccount> expandedAccounts = new ArrayList<>();
for (RewardShareData rewardShare : this.cachedOnlineRewardShares) {
if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight()) {
int groupId = BlockChain.getInstance().getMintingGroupId();
String address = rewardShare.getMinter();
boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address);
if (this.getBlockData().getHeight() < BlockChain.getInstance().getFixBatchRewardHeight())
expandedAccounts.add(new ExpandedAccount(repository, rewardShare));
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight() && isMinterGroupMember)
expandedAccounts.add(new ExpandedAccount(repository, rewardShare));
}
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight()) {
boolean isMinterGroupMember = repository.getGroupRepository().memberExists(BlockChain.getInstance().getMintingGroupId(), rewardShare.getMinter());
if (isMinterGroupMember) {
expandedAccounts.add(new ExpandedAccount(repository, rewardShare));
}
}
}
this.cachedExpandedAccounts = expandedAccounts;
LOGGER.trace(() -> String.format("Online reward-shares after expanded accounts %s", this.cachedOnlineRewardShares));
return this.cachedExpandedAccounts;
}
@ -1143,8 +1158,17 @@ public class Block {
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
for (ExpandedAccount account : expandedAccounts) {
int groupId = BlockChain.getInstance().getMintingGroupId();
String address = account.getMintingAccount().getAddress();
boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address);
if (account.getMintingAccount().getEffectiveMintingLevel() == 0)
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getFixBatchRewardHeight()) {
if (!isMinterGroupMember)
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
}
}
}
@ -1273,6 +1297,7 @@ public class Block {
// Online Accounts
ValidationResult onlineAccountsResult = this.areOnlineAccountsValid();
LOGGER.trace("Accounts valid = {}", onlineAccountsResult);
if (onlineAccountsResult != ValidationResult.OK)
return onlineAccountsResult;
@ -1361,7 +1386,7 @@ public class Block {
// Check transaction can even be processed
validationResult = transaction.isProcessable();
if (validationResult != Transaction.ValidationResult.OK) {
LOGGER.info(String.format("Error during transaction validation, tx %s: %s", Base58.encode(transactionData.getSignature()), validationResult.name()));
LOGGER.debug(String.format("Error during transaction validation, tx %s: %s", Base58.encode(transactionData.getSignature()), validationResult.name()));
return ValidationResult.TRANSACTION_INVALID;
}
@ -1562,6 +1587,7 @@ public class Block {
this.blockData.setHeight(blockchainHeight + 1);
LOGGER.trace(() -> String.format("Processing block %d", this.blockData.getHeight()));
LOGGER.trace(() -> String.format("Online Reward Shares in process %s", this.cachedOnlineRewardShares));
if (this.blockData.getHeight() > 1) {
@ -2280,7 +2306,6 @@ public class Block {
// Select the correct set of share bins based on block height
List<AccountLevelShareBin> accountLevelShareBinsForBlock = (this.blockData.getHeight() >= BlockChain.getInstance().getSharesByLevelV2Height()) ?
BlockChain.getInstance().getAccountLevelShareBinsV2() : BlockChain.getInstance().getAccountLevelShareBinsV1();
// Determine reward candidates based on account level
// This needs a deep copy, so the shares can be modified when tiers aren't activated yet
List<AccountLevelShareBin> accountLevelShareBins = new ArrayList<>();
@ -2570,9 +2595,11 @@ public class Block {
return;
int minterLevel = Account.getRewardShareEffectiveMintingLevel(this.repository, this.getMinter().getPublicKey());
String minterAddress = Account.getRewardShareMintingAddress(this.repository, this.getMinter().getPublicKey());
LOGGER.debug(String.format("======= BLOCK %d (%.8s) =======", this.getBlockData().getHeight(), Base58.encode(this.getSignature())));
LOGGER.debug(String.format("Timestamp: %d", this.getBlockData().getTimestamp()));
LOGGER.debug(String.format("Minter address: %s", minterAddress));
LOGGER.debug(String.format("Minter level: %d", minterLevel));
LOGGER.debug(String.format("Online accounts: %d", this.getBlockData().getOnlineAccountsCount()));
LOGGER.debug(String.format("AT count: %d", this.getBlockData().getATCount()));

View File

@ -1,8 +1,11 @@
package org.qortal.data.block;
import com.google.common.primitives.Bytes;
import org.qortal.account.Account;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.NTP;
@ -232,11 +235,31 @@ public class BlockData implements Serializable {
return blockTimestamp < onlineAccountSignaturesTrimmedTimestamp && blockTimestamp < currentTrimmableTimestamp;
}
public String getMinterAddressFromPublicKey() {
try (final Repository repository = RepositoryManager.getRepository()) {
return Account.getRewardShareMintingAddress(repository, this.minterPublicKey);
} catch (DataException e) {
return "Unknown";
}
}
public int getMinterLevelFromPublicKey() {
try (final Repository repository = RepositoryManager.getRepository()) {
return Account.getRewardShareEffectiveMintingLevel(repository, this.minterPublicKey);
} catch (DataException e) {
return 0;
}
}
// JAXB special
@XmlElement(name = "minterAddress")
protected String getMinterAddress() {
return Crypto.toAddress(this.minterPublicKey);
return getMinterAddressFromPublicKey();
}
@XmlElement(name = "minterLevel")
protected int getMinterLevel() {
return getMinterLevelFromPublicKey();
}
}

View File

@ -48,7 +48,7 @@ public enum Handshake {
String versionString = helloMessage.getVersionString();
Matcher matcher = peer.VERSION_PATTERN.matcher(versionString);
Matcher matcher = Peer.VERSION_PATTERN.matcher(versionString);
if (!matcher.lookingAt()) {
LOGGER.debug(() -> String.format("Peer %s sent invalid HELLO version string '%s'", peer, versionString));
return null;
@ -71,7 +71,7 @@ public enum Handshake {
// Ensure the peer is running at least the version specified in MIN_PEER_VERSION
if (!peer.isAtLeastVersion(MIN_PEER_VERSION)) {
LOGGER.debug(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString));
LOGGER.debug("Ignoring peer {} because it is on an old version ({})", peer, versionString);
return null;
}
@ -79,7 +79,7 @@ public enum Handshake {
// Ensure the peer is running at least the minimum version allowed for connections
final String minPeerVersion = Settings.getInstance().getMinPeerVersion();
if (!peer.isAtLeastVersion(minPeerVersion)) {
LOGGER.debug(String.format("Ignoring peer %s because it is on an old version (%s)", peer, versionString));
LOGGER.debug("Ignoring peer {} because it is on an old version ({})", peer, versionString);
return null;
}
}
@ -106,7 +106,7 @@ public enum Handshake {
byte[] peersPublicKey = challengeMessage.getPublicKey();
byte[] peersChallenge = challengeMessage.getChallenge();
// If public key matches our public key then we've connected to self
// If public key matches our public key, then we've connected to self
byte[] ourPublicKey = Network.getInstance().getOurPublicKey();
if (Arrays.equals(ourPublicKey, peersPublicKey)) {
// If outgoing connection then record destination as self so we don't try again
@ -121,11 +121,11 @@ public enum Handshake {
peer.disconnect("failed to send CHALLENGE to self");
/*
* We return CHALLENGE here to prevent us from closing connection. Closing
* connection currently preempts remote end from reading any pending messages,
* We return the CHALLENGE here to prevent us from closing the connection.
* Closing the connection currently preempts the remote end from reading any pending messages,
* specifically the CHALLENGE message we just sent above. When our 'remote'
* outbound counterpart reads our message, they will close both connections.
* Failing that, our connection will timeout or a future handshake error will
* Failing that, our connection will time out or a future handshake error will
* occur.
*/
return CHALLENGE;
@ -135,7 +135,7 @@ public enum Handshake {
// Are we already connected to this peer?
Peer existingPeer = Network.getInstance().getHandshakedPeerWithPublicKey(peersPublicKey);
if (existingPeer != null) {
LOGGER.info(() -> String.format("We already have a connection with peer %s - discarding", peer));
LOGGER.debug(() -> String.format("We already have a connection with peer %s - discarding", peer));
// Handshake failure - caller will deal with disconnect
return null;
}
@ -148,7 +148,7 @@ public enum Handshake {
@Override
public void action(Peer peer) {
// Send challenge
// Send a challenge
byte[] publicKey = Network.getInstance().getOurPublicKey();
byte[] challenge = peer.getOurChallenge();
@ -254,16 +254,17 @@ public enum Handshake {
private static final Logger LOGGER = LogManager.getLogger(Handshake.class);
/** Maximum allowed difference between peer's reported timestamp and when they connected, in milliseconds. */
/** The Maximum allowed difference between peer's reported timestamp and when they connected, in milliseconds. */
private static final long MAX_TIMESTAMP_DELTA = 30 * 1000L; // ms
private static final long PEER_VERSION_131 = 0x0100030001L;
/** Minimum peer version that we are allowed to communicate with */
private static final String MIN_PEER_VERSION = "4.1.1";
private static final String MIN_PEER_VERSION = "4.6.5";
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits
// Can always be made harder in the future...
private static final int POW_BUFFER_SIZE_POST_131 = 2 * 1024 * 1024; // bytes
private static final int POW_DIFFICULTY_POST_131 = 2; // leading zero bits
@ -275,12 +276,11 @@ public enum Handshake {
public final MessageType expectedMessageType;
private Handshake(MessageType expectedMessageType) {
Handshake(MessageType expectedMessageType) {
this.expectedMessageType = expectedMessageType;
}
public abstract Handshake onMessage(Peer peer, Message message);
public abstract void action(Peer peer);
}

View File

@ -80,7 +80,7 @@ public class Network {
"node.qortal.ru", "node2.qortal.ru", "node3.qortal.ru", "node.qortal.uk", "node22.qortal.org",
"cinfu1.crowetic.com", "node.cwd.systems", "bootstrap.cwd.systems", "node1.qortalnodes.live",
"node2.qortalnodes.live", "node3.qortalnodes.live", "node4.qortalnodes.live", "node5.qortalnodes.live",
"node6.qortalnodes.live", "node7.qortalnodes.live", "node8.qortalnodes.live"
"node.qortalnodes.live", "qortex.live",
};
private static final long NETWORK_EPC_KEEPALIVE = 5L; // seconds
@ -149,7 +149,7 @@ public class Network {
private final Lock mergePeersLock = new ReentrantLock();
private List<String> ourExternalIpAddressHistory = new ArrayList<>();
private final List<String> ourExternalIpAddressHistory = new ArrayList<>();
private String ourExternalIpAddress = null;
private int ourExternalPort = Settings.getInstance().getListenPort();
@ -167,7 +167,7 @@ public class Network {
ExecutorService networkExecutor = new ThreadPoolExecutor(2,
Settings.getInstance().getMaxNetworkThreadPoolSize(),
NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new SynchronousQueue<>(),
new NamedThreadFactory("Network-EPC", Settings.getInstance().getNetworkThreadPriority()));
networkEPC = new NetworkProcessor(networkExecutor);
}
@ -314,7 +314,7 @@ public class Network {
public List<Peer> getImmutableConnectedDataPeers() {
return this.getImmutableConnectedPeers().stream()
.filter(p -> p.isDataPeer())
.filter(Peer::isDataPeer)
.collect(Collectors.toList());
}
@ -346,7 +346,7 @@ public class Network {
public boolean requestDataFromPeer(String peerAddressString, byte[] signature) {
if (peerAddressString != null) {
PeerAddress peerAddress = PeerAddress.fromString(peerAddressString);
PeerData peerData = null;
PeerData peerData;
// Reuse an existing PeerData instance if it's already in the known peers list
synchronized (this.allKnownPeers) {
@ -370,9 +370,9 @@ public class Network {
// Check if we're already connected to and handshaked with this peer
Peer connectedPeer = this.getImmutableConnectedPeers().stream()
.filter(p -> p.getPeerData().getAddress().equals(peerAddress))
.findFirst()
.orElse(null);
.filter(p -> p.getPeerData().getAddress().equals(peerAddress))
.findFirst()
.orElse(null);
boolean isConnected = (connectedPeer != null);
@ -710,7 +710,7 @@ public class Network {
return true;
}
private Peer getConnectablePeer(final Long now) throws InterruptedException {
private Peer getConnectablePeer(final Long now) {
// We can't block here so use tryRepository(). We don't NEED to connect a new peer.
try (Repository repository = RepositoryManager.tryRepository()) {
if (repository == null) {
@ -807,7 +807,7 @@ public class Network {
// Find peers that have reached their maximum connection age, and disconnect them
List<Peer> peersToDisconnect = this.getImmutableConnectedPeers().stream()
.filter(peer -> !peer.isSyncInProgress())
.filter(peer -> peer.hasReachedMaxConnectionAge())
.filter(Peer::hasReachedMaxConnectionAge)
.collect(Collectors.toList());
if (peersToDisconnect != null && !peersToDisconnect.isEmpty()) {
@ -996,7 +996,7 @@ public class Network {
}
// Add to per-message thread count (first initializing to 0 if not already present)
threadsPerMessageType.computeIfAbsent(message.getType(), key -> 0);
threadsPerMessageType.putIfAbsent(message.getType(), 0);
threadsPerMessageType.computeIfPresent(message.getType(), (key, value) -> value + 1);
// Add to total thread count
@ -1037,7 +1037,7 @@ public class Network {
}
// Remove from per-message thread count (first initializing to 0 if not already present)
threadsPerMessageType.computeIfAbsent(message.getType(), key -> 0);
threadsPerMessageType.putIfAbsent(message.getType(), 0);
threadsPerMessageType.computeIfPresent(message.getType(), (key, value) -> value - 1);
// Remove from total thread count
@ -1135,7 +1135,7 @@ public class Network {
Peer existingPeer = getHandshakedPeerWithPublicKey(peer.getPeersPublicKey());
// NOTE: actual object reference compare, not Peer.equals()
if (existingPeer != peer) {
LOGGER.info("[{}] We already have a connection with peer {} - discarding",
LOGGER.debug("[{}] We already have a connection with peer {} - discarding",
peer.getPeerConnectionId(), peer);
peer.disconnect("existing connection");
return;
@ -1216,29 +1216,7 @@ public class Network {
* Returns PEERS message made from peers we've connected to recently, and this node's details
*/
public Message buildPeersMessage(Peer peer) {
List<PeerData> knownPeers = this.getAllKnownPeers();
// Filter out peers that we've not connected to ever or within X milliseconds
final long connectionThreshold = NTP.getTime() - RECENT_CONNECTION_THRESHOLD;
Predicate<PeerData> notRecentlyConnected = peerData -> {
final Long lastAttempted = peerData.getLastAttempted();
final Long lastConnected = peerData.getLastConnected();
if (lastAttempted == null || lastConnected == null) {
return true;
}
if (lastConnected < lastAttempted) {
return true;
}
if (lastConnected < connectionThreshold) {
return true;
}
return false;
};
knownPeers.removeIf(notRecentlyConnected);
final var knownPeers = getPeerData();
List<PeerAddress> peerAddresses = new ArrayList<>();
@ -1262,6 +1240,29 @@ public class Network {
return new PeersV2Message(peerAddresses);
}
private List<PeerData> getPeerData() {
List<PeerData> knownPeers = this.getAllKnownPeers();
// Filter out peers that we've not connected to ever or within X milliseconds
final long connectionThreshold = NTP.getTime() - RECENT_CONNECTION_THRESHOLD;
Predicate<PeerData> notRecentlyConnected = peerData -> {
final Long lastAttempted = peerData.getLastAttempted();
final Long lastConnected = peerData.getLastConnected();
if (lastAttempted == null || lastConnected == null) {
return true;
}
if (lastConnected < lastAttempted) {
return true;
}
return lastConnected < connectionThreshold;
};
knownPeers.removeIf(notRecentlyConnected);
return knownPeers;
}
/** Builds either (legacy) HeightV2Message or (newer) BlockSummariesV2Message, depending on peer version.
*
* @return Message, or null if DataException was thrown.
@ -1369,12 +1370,12 @@ public class Network {
for (int i = size-1; i >= 0; i--) {
String reading = ipAddressHistory.get(i);
if (lastReading != null) {
if (Objects.equals(reading, lastReading)) {
if (Objects.equals(reading, lastReading)) {
consecutiveReadings++;
}
else {
consecutiveReadings = 0;
}
}
else {
consecutiveReadings = 0;
}
}
lastReading = reading;
}
@ -1515,12 +1516,8 @@ public class Network {
return true;
}
if (peerData.getLastConnected() == null
|| peerData.getLastConnected() > now - OLD_PEER_CONNECTION_PERIOD) {
return true;
}
return false;
return peerData.getLastConnected() == null
|| peerData.getLastConnected() > now - OLD_PEER_CONNECTION_PERIOD;
};
// Disregard peers that are NOT 'old'
@ -1655,7 +1652,7 @@ public class Network {
// Stop processing threads
try {
if (!this.networkEPC.shutdown(5000)) {
if (!this.networkEPC.shutdown(10000)) {
LOGGER.warn("Network threads failed to terminate");
}
} catch (InterruptedException e) {
@ -1667,5 +1664,4 @@ public class Network {
peer.shutdown();
}
}
}