diff --git a/src/main/java/org/qortal/account/Account.java b/src/main/java/org/qortal/account/Account.java index df14d88f..c3a25fb6 100644 --- a/src/main/java/org/qortal/account/Account.java +++ b/src/main/java/org/qortal/account/Account.java @@ -8,11 +8,13 @@ import javax.xml.bind.annotation.XmlAccessorType; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qortal.block.BlockChain; +import org.qortal.controller.LiteNode; import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AccountData; import org.qortal.data.account.RewardShareData; import org.qortal.repository.DataException; import org.qortal.repository.Repository; +import org.qortal.settings.Settings; import org.qortal.utils.Base58; @XmlAccessorType(XmlAccessType.NONE) // Stops JAX-RS errors when unmarshalling blockchain config @@ -59,7 +61,17 @@ public class Account { // Balance manipulations - assetId is 0 for QORT public long getConfirmedBalance(long assetId) throws DataException { - AccountBalanceData accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId); + AccountBalanceData accountBalanceData; + + if (Settings.getInstance().isLite()) { + // Lite nodes request data from peers instead of the local db + accountBalanceData = LiteNode.getInstance().fetchAccountBalance(this.address, assetId); + } + else { + // All other node types fetch from the local db + accountBalanceData = this.repository.getAccountRepository().getBalance(this.address, assetId); + } + if (accountBalanceData == null) return 0; diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java index b5268db7..4de8d908 100644 --- a/src/main/java/org/qortal/api/resource/AddressesResource.java +++ b/src/main/java/org/qortal/api/resource/AddressesResource.java @@ -30,6 +30,7 @@ import org.qortal.api.Security; import org.qortal.api.model.ApiOnlineAccount; import org.qortal.api.model.RewardShareKeyRequest; import org.qortal.asset.Asset; +import org.qortal.controller.LiteNode; import org.qortal.controller.OnlineAccountsManager; import org.qortal.crypto.Crypto; import org.qortal.data.account.AccountData; @@ -109,18 +110,26 @@ public class AddressesResource { if (!Crypto.isValidAddress(address)) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); - byte[] lastReference = null; + AccountData accountData; - try (final Repository repository = RepositoryManager.getRepository()) { - AccountData accountData = repository.getAccountRepository().getAccount(address); - // Not found? - if (accountData == null) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); - - lastReference = accountData.getReference(); - } catch (DataException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + if (Settings.getInstance().isLite()) { + // Lite nodes request data from peers instead of the local db + accountData = LiteNode.getInstance().fetchAccountData(address); } + else { + // All other node types request data from local db + try (final Repository repository = RepositoryManager.getRepository()) { + accountData = repository.getAccountRepository().getAccount(address); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + // Not found? + if (accountData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); + + byte[] lastReference = accountData.getReference(); if (lastReference == null || lastReference.length == 0) return "false"; diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index e2425ba6..e73eecbd 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -39,6 +39,8 @@ import org.qortal.controller.arbitrary.*; import org.qortal.controller.repository.PruneManager; import org.qortal.controller.repository.NamesDatabaseIntegrityCheck; import org.qortal.controller.tradebot.TradeBot; +import org.qortal.data.account.AccountBalanceData; +import org.qortal.data.account.AccountData; import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockSummaryData; import org.qortal.data.network.PeerChainTipData; @@ -178,6 +180,28 @@ public class Controller extends Thread { } public GetArbitraryMetadataMessageStats getArbitraryMetadataMessageStats = new GetArbitraryMetadataMessageStats(); + public static class GetAccountMessageStats { + public AtomicLong requests = new AtomicLong(); + public AtomicLong cacheHits = new AtomicLong(); + public AtomicLong unknownAccounts = new AtomicLong(); + public AtomicLong cacheFills = new AtomicLong(); + + public GetAccountMessageStats() { + } + } + public GetAccountMessageStats getAccountMessageStats = new GetAccountMessageStats(); + + public static class GetAccountBalanceMessageStats { + public AtomicLong requests = new AtomicLong(); + public AtomicLong cacheHits = new AtomicLong(); + public AtomicLong unknownAccounts = new AtomicLong(); + public AtomicLong cacheFills = new AtomicLong(); + + public GetAccountBalanceMessageStats() { + } + } + public GetAccountBalanceMessageStats getAccountBalanceMessageStats = new GetAccountBalanceMessageStats(); + public AtomicLong latestBlocksCacheRefills = new AtomicLong(); public StatsSnapshot() { @@ -1232,6 +1256,14 @@ public class Controller extends Thread { TradeBot.getInstance().onTradePresencesMessage(peer, message); break; + case GET_ACCOUNT: + onNetworkGetAccountMessage(peer, message); + break; + + case GET_ACCOUNT_BALANCE: + onNetworkGetAccountBalanceMessage(peer, message); + break; + default: LOGGER.debug(() -> String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer)); break; @@ -1483,6 +1515,77 @@ public class Controller extends Thread { Synchronizer.getInstance().requestSync(); } + private void onNetworkGetAccountMessage(Peer peer, Message message) { + GetAccountMessage getAccountMessage = (GetAccountMessage) message; + String address = getAccountMessage.getAddress(); + this.stats.getAccountMessageStats.requests.incrementAndGet(); + + try (final Repository repository = RepositoryManager.getRepository()) { + AccountData accountData = repository.getAccountRepository().getAccount(address); + + if (accountData == null) { + // We don't have this account + this.stats.getAccountMessageStats.unknownAccounts.getAndIncrement(); + + // Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout + LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT request for unknown account %s", peer, address)); + + // We'll send empty block summaries message as it's very short + Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList()); + accountUnknownMessage.setId(message.getId()); + if (!peer.sendMessage(accountUnknownMessage)) + peer.disconnect("failed to send account-unknown response"); + return; + } + + AccountMessage accountMessage = new AccountMessage(accountData); + accountMessage.setId(message.getId()); + + if (!peer.sendMessage(accountMessage)) { + peer.disconnect("failed to send account"); + } + + } catch (DataException e) { + LOGGER.error(String.format("Repository issue while send account %s to peer %s", address, peer), e); + } + } + + private void onNetworkGetAccountBalanceMessage(Peer peer, Message message) { + GetAccountBalanceMessage getAccountBalanceMessage = (GetAccountBalanceMessage) message; + String address = getAccountBalanceMessage.getAddress(); + long assetId = getAccountBalanceMessage.getAssetId(); + this.stats.getAccountBalanceMessageStats.requests.incrementAndGet(); + + try (final Repository repository = RepositoryManager.getRepository()) { + AccountBalanceData accountBalanceData = repository.getAccountRepository().getBalance(address, assetId); + + if (accountBalanceData == null) { + // We don't have this account + this.stats.getAccountBalanceMessageStats.unknownAccounts.getAndIncrement(); + + // Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout + LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT request for unknown account %s and asset ID %d", peer, address, assetId)); + + // We'll send empty block summaries message as it's very short + Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList()); + accountUnknownMessage.setId(message.getId()); + if (!peer.sendMessage(accountUnknownMessage)) + peer.disconnect("failed to send account-unknown response"); + return; + } + + AccountBalanceMessage accountMessage = new AccountBalanceMessage(accountBalanceData); + accountMessage.setId(message.getId()); + + if (!peer.sendMessage(accountMessage)) { + peer.disconnect("failed to send account"); + } + + } catch (DataException e) { + LOGGER.error(String.format("Repository issue while send balance for account %s and asset ID %d to peer %s", address, assetId, peer), e); + } + } + // Utilities diff --git a/src/main/java/org/qortal/controller/LiteNode.java b/src/main/java/org/qortal/controller/LiteNode.java new file mode 100644 index 00000000..c5e6affe --- /dev/null +++ b/src/main/java/org/qortal/controller/LiteNode.java @@ -0,0 +1,117 @@ +package org.qortal.controller; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.qortal.data.account.AccountBalanceData; +import org.qortal.data.account.AccountData; +import org.qortal.network.Network; +import org.qortal.network.Peer; +import org.qortal.network.message.*; + +import java.security.SecureRandom; +import java.util.*; + +import static org.qortal.network.message.Message.MessageType; +import static org.qortal.network.message.Message.MessageType.*; + +public class LiteNode { + + private static final Logger LOGGER = LogManager.getLogger(LiteNode.class); + + private static LiteNode instance; + + + public Map pendingRequests = Collections.synchronizedMap(new HashMap<>()); + + + public LiteNode() { + + } + + public static synchronized LiteNode getInstance() { + if (instance == null) { + instance = new LiteNode(); + } + + return instance; + } + + + /** + * Fetch account data from peers for given QORT address + * @param address - the QORT address to query + * @return accountData - the account data for this address, or null if not retrieved + */ + public AccountData fetchAccountData(String address) { + GetAccountMessage getAccountMessage = new GetAccountMessage(address); + AccountMessage accountMessage = (AccountMessage) this.sendMessage(getAccountMessage, ACCOUNT); + if (accountMessage == null) { + return null; + } + return accountMessage.getAccountData(); + } + + /** + * Fetch account balance data from peers for given QORT address and asset ID + * @param address - the QORT address to query + * @return balance - the balance for this address and assetId, or null if not retrieved + */ + public AccountBalanceData fetchAccountBalance(String address, long assetId) { + GetAccountBalanceMessage getAccountMessage = new GetAccountBalanceMessage(address, assetId); + AccountBalanceMessage accountMessage = (AccountBalanceMessage) this.sendMessage(getAccountMessage, ACCOUNT_BALANCE); + if (accountMessage == null) { + return null; + } + return accountMessage.getAccountBalanceData(); + } + + + private Message sendMessage(Message message, MessageType expectedResponseMessageType) { + // This asks a random peer for the data + // TODO: ask multiple peers, and disregard everything if there are any significant differences in the responses + + // Needs a mutable copy of the unmodifiableList + List peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers()); + + // Disregard peers that have "misbehaved" recently + peers.removeIf(Controller.hasMisbehaved); + + // Disregard peers that only have genesis block + peers.removeIf(Controller.hasOnlyGenesisBlock); + + // Disregard peers that are on an old version + peers.removeIf(Controller.hasOldVersion); + + // Disregard peers that are on a known inferior chain tip + peers.removeIf(Controller.hasInferiorChainTip); + + if (peers.isEmpty()) { + LOGGER.info("No peers available to send {} message to", message.getType()); + return null; + } + + // Pick random peer + int index = new SecureRandom().nextInt(peers.size()); + Peer peer = peers.get(index); + + LOGGER.info("Sending {} message to peer {}...", message.getType(), peer); + + Message responseMessage; + + try { + responseMessage = peer.getResponse(message); + + } catch (InterruptedException e) { + return null; + } + + if (responseMessage == null || responseMessage.getType() != expectedResponseMessageType) { + return null; + } + + LOGGER.info("Peer {} responded with {} message", peer, responseMessage.getType()); + + return responseMessage; + } + +} diff --git a/src/main/java/org/qortal/network/message/AccountBalanceMessage.java b/src/main/java/org/qortal/network/message/AccountBalanceMessage.java new file mode 100644 index 00000000..5e64d2b5 --- /dev/null +++ b/src/main/java/org/qortal/network/message/AccountBalanceMessage.java @@ -0,0 +1,78 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Longs; +import org.qortal.data.account.AccountBalanceData; +import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +public class AccountBalanceMessage extends Message { + + private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH; + + private final AccountBalanceData accountBalanceData; + + public AccountBalanceMessage(AccountBalanceData accountBalanceData) { + super(MessageType.ACCOUNT_BALANCE); + + this.accountBalanceData = accountBalanceData; + } + + public AccountBalanceMessage(int id, AccountBalanceData accountBalanceData) { + super(id, MessageType.ACCOUNT_BALANCE); + + this.accountBalanceData = accountBalanceData; + } + + public AccountBalanceData getAccountBalanceData() { + return this.accountBalanceData; + } + + + public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException { + byte[] addressBytes = new byte[ADDRESS_LENGTH]; + byteBuffer.get(addressBytes); + String address = Base58.encode(addressBytes); + + long assetId = byteBuffer.getLong(); + + long balance = byteBuffer.getLong(); + + AccountBalanceData accountBalanceData = new AccountBalanceData(address, assetId, balance); + return new AccountBalanceMessage(id, accountBalanceData); + } + + @Override + protected byte[] toData() { + if (this.accountBalanceData == null) { + return null; + } + + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + // Send raw address instead of base58 encoded + byte[] address = Base58.decode(this.accountBalanceData.getAddress()); + bytes.write(address); + + bytes.write(Longs.toByteArray(this.accountBalanceData.getAssetId())); + + bytes.write(Longs.toByteArray(this.accountBalanceData.getBalance())); + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + + public AccountBalanceMessage cloneWithNewId(int newId) { + AccountBalanceMessage clone = new AccountBalanceMessage(this.accountBalanceData); + clone.setId(newId); + return clone; + } + +} diff --git a/src/main/java/org/qortal/network/message/AccountMessage.java b/src/main/java/org/qortal/network/message/AccountMessage.java new file mode 100644 index 00000000..749ec01e --- /dev/null +++ b/src/main/java/org/qortal/network/message/AccountMessage.java @@ -0,0 +1,101 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Ints; +import org.qortal.data.account.AccountData; +import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +public class AccountMessage extends Message { + + private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH; + private static final int REFERENCE_LENGTH = Transformer.SIGNATURE_LENGTH; + private static final int PUBLIC_KEY_LENGTH = Transformer.PUBLIC_KEY_LENGTH; + + private final AccountData accountData; + + public AccountMessage(AccountData accountData) { + super(MessageType.ACCOUNT); + + this.accountData = accountData; + } + + public AccountMessage(int id, AccountData accountData) { + super(id, MessageType.ACCOUNT); + + this.accountData = accountData; + } + + public AccountData getAccountData() { + return this.accountData; + } + + public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException { + byte[] addressBytes = new byte[ADDRESS_LENGTH]; + byteBuffer.get(addressBytes); + String address = Base58.encode(addressBytes); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] publicKey = new byte[PUBLIC_KEY_LENGTH]; + byteBuffer.get(publicKey); + + int defaultGroupId = byteBuffer.getInt(); + + int flags = byteBuffer.getInt(); + + int level = byteBuffer.getInt(); + + int blocksMinted = byteBuffer.getInt(); + + int blocksMintedAdjustment = byteBuffer.getInt(); + + AccountData accountData = new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment); + return new AccountMessage(id, accountData); + } + + @Override + protected byte[] toData() { + if (this.accountData == null) { + return null; + } + + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + // Send raw address instead of base58 encoded + byte[] address = Base58.decode(accountData.getAddress()); + bytes.write(address); + + bytes.write(accountData.getReference()); + + bytes.write(accountData.getPublicKey()); + + bytes.write(Ints.toByteArray(accountData.getDefaultGroupId())); + + bytes.write(Ints.toByteArray(accountData.getFlags())); + + bytes.write(Ints.toByteArray(accountData.getLevel())); + + bytes.write(Ints.toByteArray(accountData.getBlocksMinted())); + + bytes.write(Ints.toByteArray(accountData.getBlocksMintedAdjustment())); + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + + public AccountMessage cloneWithNewId(int newId) { + AccountMessage clone = new AccountMessage(this.accountData); + clone.setId(newId); + return clone; + } + +} diff --git a/src/main/java/org/qortal/network/message/GetAccountBalanceMessage.java b/src/main/java/org/qortal/network/message/GetAccountBalanceMessage.java new file mode 100644 index 00000000..c4caaa34 --- /dev/null +++ b/src/main/java/org/qortal/network/message/GetAccountBalanceMessage.java @@ -0,0 +1,65 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Longs; +import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +public class GetAccountBalanceMessage extends Message { + + private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH; + + private String address; + private long assetId; + + public GetAccountBalanceMessage(String address, long assetId) { + this(-1, address, assetId); + } + + private GetAccountBalanceMessage(int id, String address, long assetId) { + super(id, MessageType.GET_ACCOUNT_BALANCE); + + this.address = address; + this.assetId = assetId; + } + + public String getAddress() { + return this.address; + } + + public long getAssetId() { + return this.assetId; + } + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + byte[] addressBytes = new byte[ADDRESS_LENGTH]; + bytes.get(addressBytes); + String address = Base58.encode(addressBytes); + + long assetId = bytes.getLong(); + + return new GetAccountBalanceMessage(id, address, assetId); + } + + @Override + protected byte[] toData() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + // Send raw address instead of base58 encoded + byte[] address = Base58.decode(this.address); + bytes.write(address); + + bytes.write(Longs.toByteArray(this.assetId)); + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qortal/network/message/GetAccountMessage.java b/src/main/java/org/qortal/network/message/GetAccountMessage.java new file mode 100644 index 00000000..1d18117c --- /dev/null +++ b/src/main/java/org/qortal/network/message/GetAccountMessage.java @@ -0,0 +1,57 @@ +package org.qortal.network.message; + +import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +public class GetAccountMessage extends Message { + + private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH; + + private String address; + + public GetAccountMessage(String address) { + this(-1, address); + } + + private GetAccountMessage(int id, String address) { + super(id, MessageType.GET_ACCOUNT); + + this.address = address; + } + + public String getAddress() { + return this.address; + } + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + if (bytes.remaining() != ADDRESS_LENGTH) + return null; + + byte[] addressBytes = new byte[ADDRESS_LENGTH]; + bytes.get(addressBytes); + String address = Base58.encode(addressBytes); + + return new GetAccountMessage(id, address); + } + + @Override + protected byte[] toData() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + // Send raw address instead of base58 encoded + byte[] address = Base58.decode(this.address); + bytes.write(address); + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qortal/network/message/Message.java b/src/main/java/org/qortal/network/message/Message.java index b06a5133..a4667df7 100644 --- a/src/main/java/org/qortal/network/message/Message.java +++ b/src/main/java/org/qortal/network/message/Message.java @@ -99,7 +99,12 @@ public abstract class Message { GET_TRADE_PRESENCES(141), ARBITRARY_METADATA(150), - GET_ARBITRARY_METADATA(151); + GET_ARBITRARY_METADATA(151), + + ACCOUNT(160), + GET_ACCOUNT(161), + ACCOUNT_BALANCE(162), + GET_ACCOUNT_BALANCE(163); public final int value; public final Method fromByteBufferMethod;