forked from Qortal/qortal
Merge branch 'lite-node'
This commit is contained in:
commit
2d1b0fd6d0
@ -8,11 +8,13 @@ import javax.xml.bind.annotation.XmlAccessorType;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.block.BlockChain;
|
import org.qortal.block.BlockChain;
|
||||||
|
import org.qortal.controller.LiteNode;
|
||||||
import org.qortal.data.account.AccountBalanceData;
|
import org.qortal.data.account.AccountBalanceData;
|
||||||
import org.qortal.data.account.AccountData;
|
import org.qortal.data.account.AccountData;
|
||||||
import org.qortal.data.account.RewardShareData;
|
import org.qortal.data.account.RewardShareData;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
@XmlAccessorType(XmlAccessType.NONE) // Stops JAX-RS errors when unmarshalling blockchain config
|
@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
|
// Balance manipulations - assetId is 0 for QORT
|
||||||
|
|
||||||
public long getConfirmedBalance(long assetId) throws DataException {
|
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)
|
if (accountBalanceData == null)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ public class NodeInfo {
|
|||||||
public long buildTimestamp;
|
public long buildTimestamp;
|
||||||
public String nodeId;
|
public String nodeId;
|
||||||
public boolean isTestNet;
|
public boolean isTestNet;
|
||||||
|
public String type;
|
||||||
|
|
||||||
public NodeInfo() {
|
public NodeInfo() {
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ import org.qortal.api.Security;
|
|||||||
import org.qortal.api.model.ApiOnlineAccount;
|
import org.qortal.api.model.ApiOnlineAccount;
|
||||||
import org.qortal.api.model.RewardShareKeyRequest;
|
import org.qortal.api.model.RewardShareKeyRequest;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
|
import org.qortal.controller.LiteNode;
|
||||||
import org.qortal.controller.OnlineAccountsManager;
|
import org.qortal.controller.OnlineAccountsManager;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.account.AccountData;
|
import org.qortal.data.account.AccountData;
|
||||||
@ -109,18 +110,26 @@ public class AddressesResource {
|
|||||||
if (!Crypto.isValidAddress(address))
|
if (!Crypto.isValidAddress(address))
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
byte[] lastReference = null;
|
AccountData accountData;
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
if (Settings.getInstance().isLite()) {
|
||||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
// Lite nodes request data from peers instead of the local db
|
||||||
// Not found?
|
accountData = LiteNode.getInstance().fetchAccountData(address);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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)
|
if (lastReference == null || lastReference.length == 0)
|
||||||
return "false";
|
return "false";
|
||||||
|
@ -119,10 +119,23 @@ public class AdminResource {
|
|||||||
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
|
nodeInfo.buildTimestamp = Controller.getInstance().getBuildTimestamp();
|
||||||
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
|
nodeInfo.nodeId = Network.getInstance().getOurNodeId();
|
||||||
nodeInfo.isTestNet = Settings.getInstance().isTestNet();
|
nodeInfo.isTestNet = Settings.getInstance().isTestNet();
|
||||||
|
nodeInfo.type = getNodeType();
|
||||||
|
|
||||||
return nodeInfo;
|
return nodeInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getNodeType() {
|
||||||
|
if (Settings.getInstance().isTopOnly()) {
|
||||||
|
return "topOnly";
|
||||||
|
}
|
||||||
|
else if (Settings.getInstance().isLite()) {
|
||||||
|
return "lite";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return "full";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/status")
|
@Path("/status")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -26,6 +26,7 @@ import org.qortal.api.ApiErrors;
|
|||||||
import org.qortal.api.ApiException;
|
import org.qortal.api.ApiException;
|
||||||
import org.qortal.api.ApiExceptionFactory;
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
import org.qortal.api.model.NameSummary;
|
import org.qortal.api.model.NameSummary;
|
||||||
|
import org.qortal.controller.LiteNode;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.naming.NameData;
|
import org.qortal.data.naming.NameData;
|
||||||
import org.qortal.data.transaction.BuyNameTransactionData;
|
import org.qortal.data.transaction.BuyNameTransactionData;
|
||||||
@ -101,7 +102,14 @@ public class NamesResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
List<NameData> names = repository.getNameRepository().getNamesByOwner(address, limit, offset, reverse);
|
List<NameData> names;
|
||||||
|
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
names = LiteNode.getInstance().fetchAccountNames(address);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
names = repository.getNameRepository().getNamesByOwner(address, limit, offset, reverse);
|
||||||
|
}
|
||||||
|
|
||||||
return names.stream().map(NameSummary::new).collect(Collectors.toList());
|
return names.stream().map(NameSummary::new).collect(Collectors.toList());
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
@ -126,10 +134,18 @@ public class NamesResource {
|
|||||||
@ApiErrors({ApiError.NAME_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
@ApiErrors({ApiError.NAME_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||||
public NameData getName(@PathParam("name") String name) {
|
public NameData getName(@PathParam("name") String name) {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
NameData nameData = repository.getNameRepository().fromName(name);
|
NameData nameData;
|
||||||
|
|
||||||
if (nameData == null)
|
if (Settings.getInstance().isLite()) {
|
||||||
|
nameData = LiteNode.getInstance().fetchNameData(name);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nameData = repository.getNameRepository().fromName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameData == null) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NAME_UNKNOWN);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NAME_UNKNOWN);
|
||||||
|
}
|
||||||
|
|
||||||
return nameData;
|
return nameData;
|
||||||
} catch (ApiException e) {
|
} catch (ApiException e) {
|
||||||
|
@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
@ -32,6 +33,8 @@ import org.qortal.api.ApiException;
|
|||||||
import org.qortal.api.ApiExceptionFactory;
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
import org.qortal.api.model.SimpleTransactionSignRequest;
|
import org.qortal.api.model.SimpleTransactionSignRequest;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.controller.LiteNode;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.globalization.Translator;
|
import org.qortal.globalization.Translator;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
@ -366,6 +369,73 @@ public class TransactionsResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/address/{address}")
|
||||||
|
@Operation(
|
||||||
|
summary = "Returns transactions for given address",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "transactions",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = TransactionData.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public List<TransactionData> getAddressTransactions(@PathParam("address") String address,
|
||||||
|
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||||
|
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||||
|
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||||
|
if (!Crypto.isValidAddress(address)) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limit == null) {
|
||||||
|
limit = 0;
|
||||||
|
}
|
||||||
|
if (offset == null) {
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TransactionData> transactions;
|
||||||
|
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
// Fetch from network
|
||||||
|
transactions = LiteNode.getInstance().fetchAccountTransactions(address, limit, offset);
|
||||||
|
|
||||||
|
// Sort the data, since we can't guarantee the order that a peer sent it in
|
||||||
|
if (reverse) {
|
||||||
|
transactions.sort(Comparator.comparingLong(TransactionData::getTimestamp).reversed());
|
||||||
|
} else {
|
||||||
|
transactions.sort(Comparator.comparingLong(TransactionData::getTimestamp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Fetch from local db
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null,
|
||||||
|
null, null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED, limit, offset, reverse);
|
||||||
|
|
||||||
|
// Expand signatures to transactions
|
||||||
|
transactions = new ArrayList<>(signatures.size());
|
||||||
|
for (byte[] signature : signatures) {
|
||||||
|
transactions.add(repository.getTransactionRepository().fromSignature(signature));
|
||||||
|
}
|
||||||
|
} catch (ApiException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactions;
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/unitfee")
|
@Path("/unitfee")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -69,7 +69,8 @@ public class BlockChain {
|
|||||||
newBlockSigHeight,
|
newBlockSigHeight,
|
||||||
shareBinFix,
|
shareBinFix,
|
||||||
calcChainWeightTimestamp,
|
calcChainWeightTimestamp,
|
||||||
transactionV5Timestamp;
|
transactionV5Timestamp,
|
||||||
|
transactionV6Timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom transaction fees
|
// Custom transaction fees
|
||||||
@ -405,6 +406,10 @@ public class BlockChain {
|
|||||||
return this.featureTriggers.get(FeatureTrigger.transactionV5Timestamp.name()).longValue();
|
return this.featureTriggers.get(FeatureTrigger.transactionV5Timestamp.name()).longValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getTransactionV6Timestamp() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.transactionV6Timestamp.name()).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
// More complex getters for aspects that change by height or timestamp
|
// More complex getters for aspects that change by height or timestamp
|
||||||
|
|
||||||
public long getRewardAtHeight(int ourHeight) {
|
public long getRewardAtHeight(int ourHeight) {
|
||||||
|
@ -61,6 +61,11 @@ public class BlockMinter extends Thread {
|
|||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("BlockMinter");
|
Thread.currentThread().setName("BlockMinter");
|
||||||
|
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
// Lite nodes do not mint
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
|
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
|
||||||
// Wipe existing unconfirmed transactions
|
// Wipe existing unconfirmed transactions
|
||||||
|
@ -32,6 +32,7 @@ import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
|||||||
import org.qortal.api.ApiService;
|
import org.qortal.api.ApiService;
|
||||||
import org.qortal.api.DomainMapService;
|
import org.qortal.api.DomainMapService;
|
||||||
import org.qortal.api.GatewayService;
|
import org.qortal.api.GatewayService;
|
||||||
|
import org.qortal.api.resource.TransactionsResource;
|
||||||
import org.qortal.block.Block;
|
import org.qortal.block.Block;
|
||||||
import org.qortal.block.BlockChain;
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
||||||
@ -39,8 +40,11 @@ import org.qortal.controller.arbitrary.*;
|
|||||||
import org.qortal.controller.repository.PruneManager;
|
import org.qortal.controller.repository.PruneManager;
|
||||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||||
import org.qortal.controller.tradebot.TradeBot;
|
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.BlockData;
|
||||||
import org.qortal.data.block.BlockSummaryData;
|
import org.qortal.data.block.BlockSummaryData;
|
||||||
|
import org.qortal.data.naming.NameData;
|
||||||
import org.qortal.data.network.PeerChainTipData;
|
import org.qortal.data.network.PeerChainTipData;
|
||||||
import org.qortal.data.network.PeerData;
|
import org.qortal.data.network.PeerData;
|
||||||
import org.qortal.data.transaction.ChatTransactionData;
|
import org.qortal.data.transaction.ChatTransactionData;
|
||||||
@ -179,6 +183,52 @@ public class Controller extends Thread {
|
|||||||
}
|
}
|
||||||
public GetArbitraryMetadataMessageStats getArbitraryMetadataMessageStats = new GetArbitraryMetadataMessageStats();
|
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 GetAccountMessageStats() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public GetAccountMessageStats getAccountMessageStats = new GetAccountMessageStats();
|
||||||
|
|
||||||
|
public static class GetAccountBalanceMessageStats {
|
||||||
|
public AtomicLong requests = new AtomicLong();
|
||||||
|
public AtomicLong unknownAccounts = new AtomicLong();
|
||||||
|
|
||||||
|
public GetAccountBalanceMessageStats() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public GetAccountBalanceMessageStats getAccountBalanceMessageStats = new GetAccountBalanceMessageStats();
|
||||||
|
|
||||||
|
public static class GetAccountTransactionsMessageStats {
|
||||||
|
public AtomicLong requests = new AtomicLong();
|
||||||
|
public AtomicLong unknownAccounts = new AtomicLong();
|
||||||
|
|
||||||
|
public GetAccountTransactionsMessageStats() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public GetAccountTransactionsMessageStats getAccountTransactionsMessageStats = new GetAccountTransactionsMessageStats();
|
||||||
|
|
||||||
|
public static class GetAccountNamesMessageStats {
|
||||||
|
public AtomicLong requests = new AtomicLong();
|
||||||
|
public AtomicLong unknownAccounts = new AtomicLong();
|
||||||
|
|
||||||
|
public GetAccountNamesMessageStats() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public GetAccountNamesMessageStats getAccountNamesMessageStats = new GetAccountNamesMessageStats();
|
||||||
|
|
||||||
|
public static class GetNameMessageStats {
|
||||||
|
public AtomicLong requests = new AtomicLong();
|
||||||
|
public AtomicLong unknownAccounts = new AtomicLong();
|
||||||
|
|
||||||
|
public GetNameMessageStats() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public GetNameMessageStats getNameMessageStats = new GetNameMessageStats();
|
||||||
|
|
||||||
public AtomicLong latestBlocksCacheRefills = new AtomicLong();
|
public AtomicLong latestBlocksCacheRefills = new AtomicLong();
|
||||||
|
|
||||||
public StatsSnapshot() {
|
public StatsSnapshot() {
|
||||||
@ -363,23 +413,27 @@ public class Controller extends Thread {
|
|||||||
return; // Not System.exit() so that GUI can display error
|
return; // Not System.exit() so that GUI can display error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild Names table and check database integrity (if enabled)
|
// If we have a non-lite node, we need to perform some startup actions
|
||||||
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
|
if (!Settings.getInstance().isLite()) {
|
||||||
namesDatabaseIntegrityCheck.rebuildAllNames();
|
|
||||||
if (Settings.getInstance().isNamesIntegrityCheckEnabled()) {
|
|
||||||
namesDatabaseIntegrityCheck.runIntegrityCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGER.info("Validating blockchain");
|
// Rebuild Names table and check database integrity (if enabled)
|
||||||
try {
|
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
|
||||||
BlockChain.validate();
|
namesDatabaseIntegrityCheck.rebuildAllNames();
|
||||||
|
if (Settings.getInstance().isNamesIntegrityCheckEnabled()) {
|
||||||
|
namesDatabaseIntegrityCheck.runIntegrityCheck();
|
||||||
|
}
|
||||||
|
|
||||||
Controller.getInstance().refillLatestBlocksCache();
|
LOGGER.info("Validating blockchain");
|
||||||
LOGGER.info(String.format("Our chain height at start-up: %d", Controller.getInstance().getChainHeight()));
|
try {
|
||||||
} catch (DataException e) {
|
BlockChain.validate();
|
||||||
LOGGER.error("Couldn't validate blockchain", e);
|
|
||||||
Gui.getInstance().fatalError("Blockchain validation issue", e);
|
Controller.getInstance().refillLatestBlocksCache();
|
||||||
return; // Not System.exit() so that GUI can display error
|
LOGGER.info(String.format("Our chain height at start-up: %d", Controller.getInstance().getChainHeight()));
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error("Couldn't validate blockchain", e);
|
||||||
|
Gui.getInstance().fatalError("Blockchain validation issue", e);
|
||||||
|
return; // Not System.exit() so that GUI can display error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import current trade bot states and minting accounts if they exist
|
// Import current trade bot states and minting accounts if they exist
|
||||||
@ -737,7 +791,11 @@ public class Controller extends Thread {
|
|||||||
final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
|
final Long minLatestBlockTimestamp = NTP.getTime() - (30 * 60 * 1000L);
|
||||||
|
|
||||||
synchronized (Synchronizer.getInstance().syncLock) {
|
synchronized (Synchronizer.getInstance().syncLock) {
|
||||||
if (this.isMintingPossible) {
|
if (Settings.getInstance().isLite()) {
|
||||||
|
actionText = Translator.INSTANCE.translate("SysTray", "LITE_NODE");
|
||||||
|
SysTray.getInstance().setTrayIcon(4);
|
||||||
|
}
|
||||||
|
else if (this.isMintingPossible) {
|
||||||
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
actionText = Translator.INSTANCE.translate("SysTray", "MINTING_ENABLED");
|
||||||
SysTray.getInstance().setTrayIcon(2);
|
SysTray.getInstance().setTrayIcon(2);
|
||||||
}
|
}
|
||||||
@ -759,7 +817,11 @@ public class Controller extends Thread {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String tooltip = String.format("%s - %d %s - %s %d", actionText, numberOfPeers, connectionsText, heightText, height) + "\n" + String.format("%s: %s", Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"), this.buildVersion);
|
String tooltip = String.format("%s - %d %s", actionText, numberOfPeers, connectionsText);
|
||||||
|
if (!Settings.getInstance().isLite()) {
|
||||||
|
tooltip.concat(String.format(" - %s %d", heightText, height));
|
||||||
|
}
|
||||||
|
tooltip.concat(String.format("\n%s: %s", Translator.INSTANCE.translate("SysTray", "BUILD_VERSION"), this.buildVersion));
|
||||||
SysTray.getInstance().setToolTipText(tooltip);
|
SysTray.getInstance().setToolTipText(tooltip);
|
||||||
|
|
||||||
this.callbackExecutor.execute(() -> {
|
this.callbackExecutor.execute(() -> {
|
||||||
@ -916,6 +978,11 @@ public class Controller extends Thread {
|
|||||||
// Callbacks for/from network
|
// Callbacks for/from network
|
||||||
|
|
||||||
public void doNetworkBroadcast() {
|
public void doNetworkBroadcast() {
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
// Lite nodes have nothing to broadcast
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Network network = Network.getInstance();
|
Network network = Network.getInstance();
|
||||||
|
|
||||||
// Send (if outbound) / Request peer lists
|
// Send (if outbound) / Request peer lists
|
||||||
@ -1198,6 +1265,26 @@ public class Controller extends Thread {
|
|||||||
TradeBot.getInstance().onTradePresencesMessage(peer, message);
|
TradeBot.getInstance().onTradePresencesMessage(peer, message);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case GET_ACCOUNT:
|
||||||
|
onNetworkGetAccountMessage(peer, message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GET_ACCOUNT_BALANCE:
|
||||||
|
onNetworkGetAccountBalanceMessage(peer, message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GET_ACCOUNT_TRANSACTIONS:
|
||||||
|
onNetworkGetAccountTransactionsMessage(peer, message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GET_ACCOUNT_NAMES:
|
||||||
|
onNetworkGetAccountNamesMessage(peer, message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case GET_NAME:
|
||||||
|
onNetworkGetNameMessage(peer, message);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
LOGGER.debug(() -> String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer));
|
LOGGER.debug(() -> String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer));
|
||||||
break;
|
break;
|
||||||
@ -1434,11 +1521,13 @@ public class Controller extends Thread {
|
|||||||
private void onNetworkHeightV2Message(Peer peer, Message message) {
|
private void onNetworkHeightV2Message(Peer peer, Message message) {
|
||||||
HeightV2Message heightV2Message = (HeightV2Message) message;
|
HeightV2Message heightV2Message = (HeightV2Message) message;
|
||||||
|
|
||||||
// If peer is inbound and we've not updated their height
|
if (!Settings.getInstance().isLite()) {
|
||||||
// then this is probably their initial HEIGHT_V2 message
|
// If peer is inbound and we've not updated their height
|
||||||
// so they need a corresponding HEIGHT_V2 message from us
|
// then this is probably their initial HEIGHT_V2 message
|
||||||
if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null))
|
// so they need a corresponding HEIGHT_V2 message from us
|
||||||
peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip()));
|
if (!peer.isOutbound() && (peer.getChainTipData() == null || peer.getChainTipData().getLastHeight() == null))
|
||||||
|
peer.sendMessage(Network.getInstance().buildHeightMessage(peer, getChainTip()));
|
||||||
|
}
|
||||||
|
|
||||||
// Update peer chain tip data
|
// Update peer chain tip data
|
||||||
PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getMinterPublicKey());
|
PeerChainTipData newChainTipData = new PeerChainTipData(heightV2Message.getHeight(), heightV2Message.getSignature(), heightV2Message.getTimestamp(), heightV2Message.getMinterPublicKey());
|
||||||
@ -1448,6 +1537,193 @@ public class Controller extends Thread {
|
|||||||
Synchronizer.getInstance().requestSync();
|
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_BALANCE 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 balance");
|
||||||
|
}
|
||||||
|
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onNetworkGetAccountTransactionsMessage(Peer peer, Message message) {
|
||||||
|
GetAccountTransactionsMessage getAccountTransactionsMessage = (GetAccountTransactionsMessage) message;
|
||||||
|
String address = getAccountTransactionsMessage.getAddress();
|
||||||
|
int limit = Math.min(getAccountTransactionsMessage.getLimit(), 100);
|
||||||
|
int offset = getAccountTransactionsMessage.getOffset();
|
||||||
|
this.stats.getAccountTransactionsMessageStats.requests.incrementAndGet();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null,
|
||||||
|
null, null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED, limit, offset, false);
|
||||||
|
|
||||||
|
// Expand signatures to transactions
|
||||||
|
List<TransactionData> transactions = new ArrayList<>(signatures.size());
|
||||||
|
for (byte[] signature : signatures) {
|
||||||
|
transactions.add(repository.getTransactionRepository().fromSignature(signature));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transactions == null) {
|
||||||
|
// We don't have this account
|
||||||
|
this.stats.getAccountTransactionsMessageStats.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_TRANSACTIONS 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionsMessage transactionsMessage = new TransactionsMessage(transactions);
|
||||||
|
transactionsMessage.setId(message.getId());
|
||||||
|
|
||||||
|
if (!peer.sendMessage(transactionsMessage)) {
|
||||||
|
peer.disconnect("failed to send account transactions");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error(String.format("Repository issue while sending transactions for account %s %d to peer %s", address, peer), e);
|
||||||
|
} catch (MessageException e) {
|
||||||
|
LOGGER.error(String.format("Message serialization issue while sending transactions for account %s %d to peer %s", address, peer), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onNetworkGetAccountNamesMessage(Peer peer, Message message) {
|
||||||
|
GetAccountNamesMessage getAccountNamesMessage = (GetAccountNamesMessage) message;
|
||||||
|
String address = getAccountNamesMessage.getAddress();
|
||||||
|
this.stats.getAccountNamesMessageStats.requests.incrementAndGet();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
List<NameData> namesDataList = repository.getNameRepository().getNamesByOwner(address);
|
||||||
|
|
||||||
|
if (namesDataList == null) {
|
||||||
|
// We don't have this account
|
||||||
|
this.stats.getAccountNamesMessageStats.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_NAMES 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
NamesMessage namesMessage = new NamesMessage(namesDataList);
|
||||||
|
namesMessage.setId(message.getId());
|
||||||
|
|
||||||
|
if (!peer.sendMessage(namesMessage)) {
|
||||||
|
peer.disconnect("failed to send account names");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error(String.format("Repository issue while send names for account %s to peer %s", address, peer), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onNetworkGetNameMessage(Peer peer, Message message) {
|
||||||
|
GetNameMessage getNameMessage = (GetNameMessage) message;
|
||||||
|
String name = getNameMessage.getName();
|
||||||
|
this.stats.getNameMessageStats.requests.incrementAndGet();
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
NameData nameData = repository.getNameRepository().fromName(name);
|
||||||
|
|
||||||
|
if (nameData == null) {
|
||||||
|
// We don't have this account
|
||||||
|
this.stats.getNameMessageStats.unknownAccounts.getAndIncrement();
|
||||||
|
|
||||||
|
// Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout
|
||||||
|
LOGGER.debug(() -> String.format("Sending 'name unknown' response to peer %s for GET_NAME request for unknown name %s", peer, name));
|
||||||
|
|
||||||
|
// We'll send empty block summaries message as it's very short
|
||||||
|
Message nameUnknownMessage = new BlockSummariesMessage(Collections.emptyList());
|
||||||
|
nameUnknownMessage.setId(message.getId());
|
||||||
|
if (!peer.sendMessage(nameUnknownMessage))
|
||||||
|
peer.disconnect("failed to send name-unknown response");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NamesMessage namesMessage = new NamesMessage(Arrays.asList(nameData));
|
||||||
|
namesMessage.setId(message.getId());
|
||||||
|
|
||||||
|
if (!peer.sendMessage(namesMessage)) {
|
||||||
|
peer.disconnect("failed to send name data");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.error(String.format("Repository issue while send name %s to peer %s", name, peer), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Utilities
|
// Utilities
|
||||||
|
|
||||||
@ -1499,6 +1775,11 @@ public class Controller extends Thread {
|
|||||||
* @return boolean - whether our node's blockchain is up to date or not
|
* @return boolean - whether our node's blockchain is up to date or not
|
||||||
*/
|
*/
|
||||||
public boolean isUpToDate(Long minLatestBlockTimestamp) {
|
public boolean isUpToDate(Long minLatestBlockTimestamp) {
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
// Lite nodes are always "up to date"
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Do we even have a vaguely recent block?
|
// Do we even have a vaguely recent block?
|
||||||
if (minLatestBlockTimestamp == null)
|
if (minLatestBlockTimestamp == null)
|
||||||
return false;
|
return false;
|
||||||
|
189
src/main/java/org/qortal/controller/LiteNode.java
Normal file
189
src/main/java/org/qortal/controller/LiteNode.java
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
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.data.naming.NameData;
|
||||||
|
import org.qortal.data.transaction.TransactionData;
|
||||||
|
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.MessageType.*;
|
||||||
|
|
||||||
|
public class LiteNode {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(LiteNode.class);
|
||||||
|
|
||||||
|
private static LiteNode instance;
|
||||||
|
|
||||||
|
|
||||||
|
public Map<Integer, Long> pendingRequests = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
|
||||||
|
public int MAX_TRANSACTIONS_PER_MESSAGE = 100;
|
||||||
|
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch list of transactions for given QORT address
|
||||||
|
* @param address - the QORT address to query
|
||||||
|
* @param limit - the maximum number of results to return
|
||||||
|
* @param offset - the starting index
|
||||||
|
* @return a list of TransactionData objects, or null if not retrieved
|
||||||
|
*/
|
||||||
|
public List<TransactionData> fetchAccountTransactions(String address, int limit, int offset) {
|
||||||
|
List<TransactionData> allTransactions = new ArrayList<>();
|
||||||
|
if (limit == 0) {
|
||||||
|
limit = Integer.MAX_VALUE;
|
||||||
|
}
|
||||||
|
int batchSize = Math.min(limit, MAX_TRANSACTIONS_PER_MESSAGE);
|
||||||
|
|
||||||
|
while (allTransactions.size() < limit) {
|
||||||
|
GetAccountTransactionsMessage getAccountTransactionsMessage = new GetAccountTransactionsMessage(address, batchSize, offset);
|
||||||
|
TransactionsMessage transactionsMessage = (TransactionsMessage) this.sendMessage(getAccountTransactionsMessage, TRANSACTIONS);
|
||||||
|
if (transactionsMessage == null) {
|
||||||
|
// An error occurred, so give up instead of returning partial results
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
allTransactions.addAll(transactionsMessage.getTransactions());
|
||||||
|
if (transactionsMessage.getTransactions().size() < batchSize) {
|
||||||
|
// No more transactions to fetch
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += batchSize;
|
||||||
|
}
|
||||||
|
return allTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch list of names for given QORT address
|
||||||
|
* @param address - the QORT address to query
|
||||||
|
* @return a list of NameData objects, or null if not retrieved
|
||||||
|
*/
|
||||||
|
public List<NameData> fetchAccountNames(String address) {
|
||||||
|
GetAccountNamesMessage getAccountNamesMessage = new GetAccountNamesMessage(address);
|
||||||
|
NamesMessage namesMessage = (NamesMessage) this.sendMessage(getAccountNamesMessage, NAMES);
|
||||||
|
if (namesMessage == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return namesMessage.getNameDataList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch info about a registered name
|
||||||
|
* @param name - the name to query
|
||||||
|
* @return a NameData object, or null if not retrieved
|
||||||
|
*/
|
||||||
|
public NameData fetchNameData(String name) {
|
||||||
|
GetNameMessage getNameMessage = new GetNameMessage(name);
|
||||||
|
NamesMessage namesMessage = (NamesMessage) this.sendMessage(getNameMessage, NAMES);
|
||||||
|
if (namesMessage == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<NameData> nameDataList = namesMessage.getNameDataList();
|
||||||
|
if (nameDataList == null || nameDataList.size() != 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// We are only expecting a single item in the list
|
||||||
|
return nameDataList.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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<Peer> peers = new ArrayList<>(Network.getInstance().getImmutableHandshakedPeers());
|
||||||
|
|
||||||
|
// Disregard peers that have "misbehaved" recently
|
||||||
|
peers.removeIf(Controller.hasMisbehaved);
|
||||||
|
|
||||||
|
// Disregard peers that only have genesis block
|
||||||
|
// TODO: 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
|
||||||
|
// TODO: 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) {
|
||||||
|
LOGGER.info("Peer didn't respond to {} message", peer, message.getType());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else if (responseMessage.getType() != expectedResponseMessageType) {
|
||||||
|
LOGGER.info("Peer responded with unexpected message type {} (should be {})", peer, responseMessage.getType(), expectedResponseMessageType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Peer {} responded with {} message", peer, responseMessage.getType());
|
||||||
|
|
||||||
|
return responseMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -134,6 +134,11 @@ public class Synchronizer extends Thread {
|
|||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("Synchronizer");
|
Thread.currentThread().setName("Synchronizer");
|
||||||
|
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
// Lite nodes don't need to sync
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (running && !Controller.isStopping()) {
|
while (running && !Controller.isStopping()) {
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
|
@ -11,6 +11,7 @@ import org.qortal.network.message.TransactionSignaturesMessage;
|
|||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryManager;
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
import org.qortal.transaction.Transaction;
|
import org.qortal.transaction.Transaction;
|
||||||
import org.qortal.transform.TransformationException;
|
import org.qortal.transform.TransformationException;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
@ -55,6 +56,8 @@ public class TransactionImporter extends Thread {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
Thread.currentThread().setName("Transaction Importer");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (!Controller.isStopping()) {
|
while (!Controller.isStopping()) {
|
||||||
Thread.sleep(1000L);
|
Thread.sleep(1000L);
|
||||||
@ -106,6 +109,8 @@ public class TransactionImporter extends Thread {
|
|||||||
|
|
||||||
List<Transaction> sigValidTransactions = new ArrayList<>();
|
List<Transaction> sigValidTransactions = new ArrayList<>();
|
||||||
|
|
||||||
|
boolean isLiteNode = Settings.getInstance().isLite();
|
||||||
|
|
||||||
// Signature validation round - does not require blockchain lock
|
// Signature validation round - does not require blockchain lock
|
||||||
for (Map.Entry<TransactionData, Boolean> transactionEntry : incomingTransactionsCopy.entrySet()) {
|
for (Map.Entry<TransactionData, Boolean> transactionEntry : incomingTransactionsCopy.entrySet()) {
|
||||||
// Quick exit?
|
// Quick exit?
|
||||||
@ -119,6 +124,12 @@ public class TransactionImporter extends Thread {
|
|||||||
// Only validate signature if we haven't already done so
|
// Only validate signature if we haven't already done so
|
||||||
Boolean isSigValid = transactionEntry.getValue();
|
Boolean isSigValid = transactionEntry.getValue();
|
||||||
if (!Boolean.TRUE.equals(isSigValid)) {
|
if (!Boolean.TRUE.equals(isSigValid)) {
|
||||||
|
if (isLiteNode) {
|
||||||
|
// Lite nodes can't easily validate transactions, so for now we will have to assume that everything is valid
|
||||||
|
sigValidTransactions.add(transaction);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!transaction.isSignatureValid()) {
|
if (!transaction.isSignatureValid()) {
|
||||||
String signature58 = Base58.encode(transactionData.getSignature());
|
String signature58 = Base58.encode(transactionData.getSignature());
|
||||||
|
|
||||||
|
@ -19,6 +19,11 @@ public class AtStatesPruner implements Runnable {
|
|||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("AT States pruner");
|
Thread.currentThread().setName("AT States pruner");
|
||||||
|
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
// Nothing to prune in lite mode
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
boolean archiveMode = false;
|
boolean archiveMode = false;
|
||||||
if (!Settings.getInstance().isTopOnly()) {
|
if (!Settings.getInstance().isTopOnly()) {
|
||||||
// Top-only mode isn't enabled, but we might want to prune for the purposes of archiving
|
// Top-only mode isn't enabled, but we might want to prune for the purposes of archiving
|
||||||
|
@ -19,6 +19,11 @@ public class AtStatesTrimmer implements Runnable {
|
|||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("AT States trimmer");
|
Thread.currentThread().setName("AT States trimmer");
|
||||||
|
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
// Nothing to trim in lite mode
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
|
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ public class BlockArchiver implements Runnable {
|
|||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("Block archiver");
|
Thread.currentThread().setName("Block archiver");
|
||||||
|
|
||||||
if (!Settings.getInstance().isArchiveEnabled()) {
|
if (!Settings.getInstance().isArchiveEnabled() || Settings.getInstance().isLite()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,11 @@ public class BlockPruner implements Runnable {
|
|||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("Block pruner");
|
Thread.currentThread().setName("Block pruner");
|
||||||
|
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
// Nothing to prune in lite mode
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
boolean archiveMode = false;
|
boolean archiveMode = false;
|
||||||
if (!Settings.getInstance().isTopOnly()) {
|
if (!Settings.getInstance().isTopOnly()) {
|
||||||
// Top-only mode isn't enabled, but we might want to prune for the purposes of archiving
|
// Top-only mode isn't enabled, but we might want to prune for the purposes of archiving
|
||||||
|
@ -21,6 +21,11 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
|
|||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("Online Accounts trimmer");
|
Thread.currentThread().setName("Online Accounts trimmer");
|
||||||
|
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
// Nothing to trim in lite mode
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
// Don't even start trimming until initial rush has ended
|
// Don't even start trimming until initial rush has ended
|
||||||
Thread.sleep(INITIAL_SLEEP_PERIOD);
|
Thread.sleep(INITIAL_SLEEP_PERIOD);
|
||||||
|
@ -1084,11 +1084,13 @@ public class Network {
|
|||||||
// (If inbound sent anything here, it's possible it could be processed out-of-order with handshake message).
|
// (If inbound sent anything here, it's possible it could be processed out-of-order with handshake message).
|
||||||
|
|
||||||
if (peer.isOutbound()) {
|
if (peer.isOutbound()) {
|
||||||
// Send our height
|
if (!Settings.getInstance().isLite()) {
|
||||||
Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip());
|
// Send our height
|
||||||
if (!peer.sendMessage(heightMessage)) {
|
Message heightMessage = buildHeightMessage(peer, Controller.getInstance().getChainTip());
|
||||||
peer.disconnect("failed to send height/info");
|
if (!peer.sendMessage(heightMessage)) {
|
||||||
return;
|
peer.disconnect("failed to send height/info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send our peers list
|
// Send our peers list
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
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.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class AccountBalanceMessage extends Message {
|
||||||
|
|
||||||
|
private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH;
|
||||||
|
|
||||||
|
private AccountBalanceData accountBalanceData;
|
||||||
|
|
||||||
|
public AccountBalanceMessage(AccountBalanceData accountBalanceData) {
|
||||||
|
super(MessageType.ACCOUNT_BALANCE);
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send raw address instead of base58 encoded
|
||||||
|
byte[] address = Base58.decode(accountBalanceData.getAddress());
|
||||||
|
bytes.write(address);
|
||||||
|
|
||||||
|
bytes.write(Longs.toByteArray(accountBalanceData.getAssetId()));
|
||||||
|
|
||||||
|
bytes.write(Longs.toByteArray(accountBalanceData.getBalance()));
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataBytes = bytes.toByteArray();
|
||||||
|
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountBalanceMessage cloneWithNewId(int newId) {
|
||||||
|
AccountBalanceMessage clone = new AccountBalanceMessage(this.accountBalanceData);
|
||||||
|
clone.setId(newId);
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
93
src/main/java/org/qortal/network/message/AccountMessage.java
Normal file
93
src/main/java/org/qortal/network/message/AccountMessage.java
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
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.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 AccountData accountData;
|
||||||
|
|
||||||
|
public AccountMessage(AccountData accountData) {
|
||||||
|
super(MessageType.ACCOUNT);
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 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()));
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataBytes = bytes.toByteArray();
|
||||||
|
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountMessage cloneWithNewId(int newId) {
|
||||||
|
AccountMessage clone = new AccountMessage(this.accountData);
|
||||||
|
clone.setId(newId);
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
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.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) {
|
||||||
|
super(MessageType.GET_ACCOUNT_BALANCE);
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send raw address instead of base58 encoded
|
||||||
|
byte[] addressBytes = Base58.decode(address);
|
||||||
|
bytes.write(addressBytes);
|
||||||
|
|
||||||
|
bytes.write(Longs.toByteArray(assetId));
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataBytes = bytes.toByteArray();
|
||||||
|
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
byte[] addressBytes = new byte[ADDRESS_LENGTH];
|
||||||
|
bytes.get(addressBytes);
|
||||||
|
String address = Base58.encode(addressBytes);
|
||||||
|
|
||||||
|
long assetId = bytes.getLong();
|
||||||
|
|
||||||
|
return new GetAccountBalanceMessage(id, address, assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
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.nio.BufferUnderflowException;
|
||||||
|
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) {
|
||||||
|
super(MessageType.GET_ACCOUNT);
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send raw address instead of base58 encoded
|
||||||
|
byte[] addressBytes = Base58.decode(address);
|
||||||
|
bytes.write(addressBytes);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataBytes = bytes.toByteArray();
|
||||||
|
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (bytes.remaining() != ADDRESS_LENGTH)
|
||||||
|
throw new BufferUnderflowException();
|
||||||
|
|
||||||
|
byte[] addressBytes = new byte[ADDRESS_LENGTH];
|
||||||
|
bytes.get(addressBytes);
|
||||||
|
String address = Base58.encode(addressBytes);
|
||||||
|
|
||||||
|
return new GetAccountMessage(id, address);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
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.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class GetAccountNamesMessage extends Message {
|
||||||
|
|
||||||
|
private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH;
|
||||||
|
|
||||||
|
private String address;
|
||||||
|
|
||||||
|
public GetAccountNamesMessage(String address) {
|
||||||
|
super(MessageType.GET_ACCOUNT_NAMES);
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send raw address instead of base58 encoded
|
||||||
|
byte[] addressBytes = Base58.decode(address);
|
||||||
|
bytes.write(addressBytes);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataBytes = bytes.toByteArray();
|
||||||
|
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GetAccountNamesMessage(int id, String address) {
|
||||||
|
super(id, MessageType.GET_ACCOUNT_NAMES);
|
||||||
|
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return this.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||||
|
byte[] addressBytes = new byte[ADDRESS_LENGTH];
|
||||||
|
bytes.get(addressBytes);
|
||||||
|
String address = Base58.encode(addressBytes);
|
||||||
|
|
||||||
|
return new GetAccountNamesMessage(id, address);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package org.qortal.network.message;
|
||||||
|
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
|
import org.qortal.transform.Transformer;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class GetAccountTransactionsMessage extends Message {
|
||||||
|
|
||||||
|
private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH;
|
||||||
|
|
||||||
|
private String address;
|
||||||
|
private int limit;
|
||||||
|
private int offset;
|
||||||
|
|
||||||
|
public GetAccountTransactionsMessage(String address, int limit, int offset) {
|
||||||
|
super(MessageType.GET_ACCOUNT_TRANSACTIONS);
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send raw address instead of base58 encoded
|
||||||
|
byte[] addressBytes = Base58.decode(address);
|
||||||
|
bytes.write(addressBytes);
|
||||||
|
|
||||||
|
bytes.write(Ints.toByteArray(limit));
|
||||||
|
|
||||||
|
bytes.write(Ints.toByteArray(offset));
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataBytes = bytes.toByteArray();
|
||||||
|
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GetAccountTransactionsMessage(int id, String address, int limit, int offset) {
|
||||||
|
super(id, MessageType.GET_ACCOUNT_TRANSACTIONS);
|
||||||
|
|
||||||
|
this.address = address;
|
||||||
|
this.limit = limit;
|
||||||
|
this.offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return this.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLimit() { return this.limit; }
|
||||||
|
|
||||||
|
public int getOffset() { return this.offset; }
|
||||||
|
|
||||||
|
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
|
||||||
|
byte[] addressBytes = new byte[ADDRESS_LENGTH];
|
||||||
|
bytes.get(addressBytes);
|
||||||
|
String address = Base58.encode(addressBytes);
|
||||||
|
|
||||||
|
int limit = bytes.getInt();
|
||||||
|
|
||||||
|
int offset = bytes.getInt();
|
||||||
|
|
||||||
|
return new GetAccountTransactionsMessage(id, address, limit, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
53
src/main/java/org/qortal/network/message/GetNameMessage.java
Normal file
53
src/main/java/org/qortal/network/message/GetNameMessage.java
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package org.qortal.network.message;
|
||||||
|
|
||||||
|
import org.qortal.naming.Name;
|
||||||
|
import org.qortal.transform.TransformationException;
|
||||||
|
import org.qortal.utils.Serialization;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class GetNameMessage extends Message {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public GetNameMessage(String address) {
|
||||||
|
super(MessageType.GET_NAME);
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Serialization.serializeSizedStringV2(bytes, name);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataBytes = bytes.toByteArray();
|
||||||
|
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GetNameMessage(int id, String name) {
|
||||||
|
super(id, MessageType.GET_NAME);
|
||||||
|
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
|
||||||
|
try {
|
||||||
|
String name = Serialization.deserializeSizedStringV2(bytes, Name.MAX_NAME_SIZE);
|
||||||
|
|
||||||
|
return new GetNameMessage(id, name);
|
||||||
|
|
||||||
|
} catch (TransformationException e) {
|
||||||
|
throw new MessageException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -61,7 +61,21 @@ public enum MessageType {
|
|||||||
GET_TRADE_PRESENCES(141, GetTradePresencesMessage::fromByteBuffer),
|
GET_TRADE_PRESENCES(141, GetTradePresencesMessage::fromByteBuffer),
|
||||||
|
|
||||||
ARBITRARY_METADATA(150, ArbitraryMetadataMessage::fromByteBuffer),
|
ARBITRARY_METADATA(150, ArbitraryMetadataMessage::fromByteBuffer),
|
||||||
GET_ARBITRARY_METADATA(151, GetArbitraryMetadataMessage::fromByteBuffer);
|
GET_ARBITRARY_METADATA(151, GetArbitraryMetadataMessage::fromByteBuffer),
|
||||||
|
|
||||||
|
// Lite node support
|
||||||
|
ACCOUNT(160, AccountMessage::fromByteBuffer),
|
||||||
|
GET_ACCOUNT(161, GetAccountMessage::fromByteBuffer),
|
||||||
|
|
||||||
|
ACCOUNT_BALANCE(170, AccountBalanceMessage::fromByteBuffer),
|
||||||
|
GET_ACCOUNT_BALANCE(171, GetAccountBalanceMessage::fromByteBuffer),
|
||||||
|
|
||||||
|
NAMES(180, NamesMessage::fromByteBuffer),
|
||||||
|
GET_ACCOUNT_NAMES(181, GetAccountNamesMessage::fromByteBuffer),
|
||||||
|
GET_NAME(182, GetNameMessage::fromByteBuffer),
|
||||||
|
|
||||||
|
TRANSACTIONS(190, TransactionsMessage::fromByteBuffer),
|
||||||
|
GET_ACCOUNT_TRANSACTIONS(191, GetAccountTransactionsMessage::fromByteBuffer);
|
||||||
|
|
||||||
public final int value;
|
public final int value;
|
||||||
public final MessageProducer fromByteBufferMethod;
|
public final MessageProducer fromByteBufferMethod;
|
||||||
|
142
src/main/java/org/qortal/network/message/NamesMessage.java
Normal file
142
src/main/java/org/qortal/network/message/NamesMessage.java
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
package org.qortal.network.message;
|
||||||
|
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
|
import com.google.common.primitives.Longs;
|
||||||
|
import org.qortal.data.naming.NameData;
|
||||||
|
import org.qortal.naming.Name;
|
||||||
|
import org.qortal.transform.TransformationException;
|
||||||
|
import org.qortal.transform.Transformer;
|
||||||
|
import org.qortal.utils.Serialization;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class NamesMessage extends Message {
|
||||||
|
|
||||||
|
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
|
||||||
|
|
||||||
|
private List<NameData> nameDataList;
|
||||||
|
|
||||||
|
public NamesMessage(List<NameData> nameDataList) {
|
||||||
|
super(MessageType.NAMES);
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
bytes.write(Ints.toByteArray(nameDataList.size()));
|
||||||
|
|
||||||
|
for (int i = 0; i < nameDataList.size(); ++i) {
|
||||||
|
NameData nameData = nameDataList.get(i);
|
||||||
|
|
||||||
|
Serialization.serializeSizedStringV2(bytes, nameData.getName());
|
||||||
|
|
||||||
|
Serialization.serializeSizedStringV2(bytes, nameData.getReducedName());
|
||||||
|
|
||||||
|
Serialization.serializeAddress(bytes, nameData.getOwner());
|
||||||
|
|
||||||
|
Serialization.serializeSizedStringV2(bytes, nameData.getData());
|
||||||
|
|
||||||
|
bytes.write(Longs.toByteArray(nameData.getRegistered()));
|
||||||
|
|
||||||
|
Long updated = nameData.getUpdated();
|
||||||
|
int wasUpdated = (updated != null) ? 1 : 0;
|
||||||
|
bytes.write(Ints.toByteArray(wasUpdated));
|
||||||
|
|
||||||
|
if (updated != null) {
|
||||||
|
bytes.write(Longs.toByteArray(nameData.getUpdated()));
|
||||||
|
}
|
||||||
|
|
||||||
|
int isForSale = nameData.isForSale() ? 1 : 0;
|
||||||
|
bytes.write(Ints.toByteArray(isForSale));
|
||||||
|
|
||||||
|
if (nameData.isForSale()) {
|
||||||
|
bytes.write(Longs.toByteArray(nameData.getSalePrice()));
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes.write(nameData.getReference());
|
||||||
|
|
||||||
|
bytes.write(Ints.toByteArray(nameData.getCreationGroupId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataBytes = bytes.toByteArray();
|
||||||
|
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NamesMessage(int id, List<NameData> nameDataList) {
|
||||||
|
super(id, MessageType.NAMES);
|
||||||
|
|
||||||
|
this.nameDataList = nameDataList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<NameData> getNameDataList() {
|
||||||
|
return this.nameDataList;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
|
||||||
|
try {
|
||||||
|
final int nameCount = bytes.getInt();
|
||||||
|
|
||||||
|
List<NameData> nameDataList = new ArrayList<>(nameCount);
|
||||||
|
|
||||||
|
for (int i = 0; i < nameCount; ++i) {
|
||||||
|
String name = Serialization.deserializeSizedStringV2(bytes, Name.MAX_NAME_SIZE);
|
||||||
|
|
||||||
|
String reducedName = Serialization.deserializeSizedStringV2(bytes, Name.MAX_NAME_SIZE);
|
||||||
|
|
||||||
|
String owner = Serialization.deserializeAddress(bytes);
|
||||||
|
|
||||||
|
String data = Serialization.deserializeSizedStringV2(bytes, Name.MAX_DATA_SIZE);
|
||||||
|
|
||||||
|
long registered = bytes.getLong();
|
||||||
|
|
||||||
|
int wasUpdated = bytes.getInt();
|
||||||
|
|
||||||
|
Long updated = null;
|
||||||
|
if (wasUpdated == 1) {
|
||||||
|
updated = bytes.getLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isForSale = (bytes.getInt() == 1);
|
||||||
|
|
||||||
|
Long salePrice = null;
|
||||||
|
if (isForSale) {
|
||||||
|
salePrice = bytes.getLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] reference = new byte[SIGNATURE_LENGTH];
|
||||||
|
bytes.get(reference);
|
||||||
|
|
||||||
|
int creationGroupId = bytes.getInt();
|
||||||
|
|
||||||
|
NameData nameData = new NameData(name, reducedName, owner, data, registered, updated,
|
||||||
|
isForSale, salePrice, reference, creationGroupId);
|
||||||
|
nameDataList.add(nameData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes.hasRemaining()) {
|
||||||
|
throw new BufferUnderflowException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NamesMessage(id, nameDataList);
|
||||||
|
|
||||||
|
} catch (TransformationException e) {
|
||||||
|
throw new MessageException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public NamesMessage cloneWithNewId(int newId) {
|
||||||
|
NamesMessage clone = new NamesMessage(this.nameDataList);
|
||||||
|
clone.setId(newId);
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package org.qortal.network.message;
|
||||||
|
|
||||||
|
import com.google.common.primitives.Ints;
|
||||||
|
import org.qortal.data.transaction.TransactionData;
|
||||||
|
import org.qortal.transform.TransformationException;
|
||||||
|
import org.qortal.transform.transaction.TransactionTransformer;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.BufferUnderflowException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TransactionsMessage extends Message {
|
||||||
|
|
||||||
|
private List<TransactionData> transactions;
|
||||||
|
|
||||||
|
public TransactionsMessage(List<TransactionData> transactions) throws MessageException {
|
||||||
|
super(MessageType.TRANSACTIONS);
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
try {
|
||||||
|
bytes.write(Ints.toByteArray(transactions.size()));
|
||||||
|
|
||||||
|
for (int i = 0; i < transactions.size(); ++i) {
|
||||||
|
TransactionData transactionData = transactions.get(i);
|
||||||
|
|
||||||
|
byte[] serializedTransactionData = TransactionTransformer.toBytes(transactionData);
|
||||||
|
bytes.write(serializedTransactionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||||
|
} catch (TransformationException e) {
|
||||||
|
throw new MessageException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataBytes = bytes.toByteArray();
|
||||||
|
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransactionsMessage(int id, List<TransactionData> transactions) {
|
||||||
|
super(id, MessageType.TRANSACTIONS);
|
||||||
|
|
||||||
|
this.transactions = transactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TransactionData> getTransactions() {
|
||||||
|
return this.transactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
|
||||||
|
try {
|
||||||
|
final int transactionCount = byteBuffer.getInt();
|
||||||
|
|
||||||
|
List<TransactionData> transactions = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < transactionCount; ++i) {
|
||||||
|
TransactionData transactionData = TransactionTransformer.fromByteBuffer(byteBuffer);
|
||||||
|
transactions.add(transactionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byteBuffer.hasRemaining()) {
|
||||||
|
throw new BufferUnderflowException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TransactionsMessage(id, transactions);
|
||||||
|
|
||||||
|
} catch (TransformationException e) {
|
||||||
|
throw new MessageException(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -62,6 +62,11 @@ public abstract class RepositoryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean archive(Repository repository) {
|
public static boolean archive(Repository repository) {
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
// Lite nodes have no blockchain
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Bulk archive the database the first time we use archive mode
|
// Bulk archive the database the first time we use archive mode
|
||||||
if (Settings.getInstance().isArchiveEnabled()) {
|
if (Settings.getInstance().isArchiveEnabled()) {
|
||||||
if (RepositoryManager.canArchiveOrPrune()) {
|
if (RepositoryManager.canArchiveOrPrune()) {
|
||||||
@ -82,6 +87,11 @@ public abstract class RepositoryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean prune(Repository repository) {
|
public static boolean prune(Repository repository) {
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
// Lite nodes have no blockchain
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Bulk prune the database the first time we use top-only or block archive mode
|
// Bulk prune the database the first time we use top-only or block archive mode
|
||||||
if (Settings.getInstance().isTopOnly() ||
|
if (Settings.getInstance().isTopOnly() ||
|
||||||
Settings.getInstance().isArchiveEnabled()) {
|
Settings.getInstance().isArchiveEnabled()) {
|
||||||
|
@ -145,6 +145,8 @@ public class Settings {
|
|||||||
* This has a significant effect on execution time. */
|
* This has a significant effect on execution time. */
|
||||||
private int onlineSignaturesTrimBatchSize = 100; // blocks
|
private int onlineSignaturesTrimBatchSize = 100; // blocks
|
||||||
|
|
||||||
|
/** Lite nodes don't sync blocks, and instead request "derived data" from peers */
|
||||||
|
private boolean lite = false;
|
||||||
|
|
||||||
/** Whether we should prune old data to reduce database size
|
/** Whether we should prune old data to reduce database size
|
||||||
* This prevents the node from being able to serve older blocks */
|
* This prevents the node from being able to serve older blocks */
|
||||||
@ -820,6 +822,10 @@ public class Settings {
|
|||||||
return this.onlineSignaturesTrimBatchSize;
|
return this.onlineSignaturesTrimBatchSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isLite() {
|
||||||
|
return this.lite;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isTopOnly() {
|
public boolean isTopOnly() {
|
||||||
return this.topOnly;
|
return this.topOnly;
|
||||||
}
|
}
|
||||||
|
@ -393,7 +393,10 @@ public abstract class Transaction {
|
|||||||
* @return transaction version number
|
* @return transaction version number
|
||||||
*/
|
*/
|
||||||
public static int getVersionByTimestamp(long timestamp) {
|
public static int getVersionByTimestamp(long timestamp) {
|
||||||
if (timestamp >= BlockChain.getInstance().getTransactionV5Timestamp()) {
|
if (timestamp >= BlockChain.getInstance().getTransactionV6Timestamp()) {
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
else if (timestamp >= BlockChain.getInstance().getTransactionV5Timestamp()) {
|
||||||
return 5;
|
return 5;
|
||||||
}
|
}
|
||||||
return 4;
|
return 4;
|
||||||
@ -530,11 +533,6 @@ public abstract class Transaction {
|
|||||||
if (now >= this.getDeadline())
|
if (now >= this.getDeadline())
|
||||||
return ValidationResult.TIMESTAMP_TOO_OLD;
|
return ValidationResult.TIMESTAMP_TOO_OLD;
|
||||||
|
|
||||||
// Transactions with a expiry prior to latest block's timestamp are too old
|
|
||||||
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
|
||||||
if (this.getDeadline() <= latestBlock.getTimestamp())
|
|
||||||
return ValidationResult.TIMESTAMP_TOO_OLD;
|
|
||||||
|
|
||||||
// Transactions with a timestamp too far into future are too new
|
// Transactions with a timestamp too far into future are too new
|
||||||
long maxTimestamp = now + Settings.getInstance().getMaxTransactionTimestampFuture();
|
long maxTimestamp = now + Settings.getInstance().getMaxTransactionTimestampFuture();
|
||||||
if (this.transactionData.getTimestamp() > maxTimestamp)
|
if (this.transactionData.getTimestamp() > maxTimestamp)
|
||||||
@ -545,6 +543,15 @@ public abstract class Transaction {
|
|||||||
if (feeValidationResult != ValidationResult.OK)
|
if (feeValidationResult != ValidationResult.OK)
|
||||||
return feeValidationResult;
|
return feeValidationResult;
|
||||||
|
|
||||||
|
if (Settings.getInstance().isLite()) {
|
||||||
|
// Everything from this point is difficult to validate for a lite node, since it has no blocks.
|
||||||
|
// For now, we will assume it is valid, to allow it to move around the network easily.
|
||||||
|
// If it turns out to be invalid, other full/top-only nodes will reject it on receipt.
|
||||||
|
// Lite nodes would never mint a block, so there's not much risk of holding invalid transactions.
|
||||||
|
// TODO: implement lite-only validation for each transaction type
|
||||||
|
return ValidationResult.OK;
|
||||||
|
}
|
||||||
|
|
||||||
PublicKeyAccount creator = this.getCreator();
|
PublicKeyAccount creator = this.getCreator();
|
||||||
if (creator == null)
|
if (creator == null)
|
||||||
return ValidationResult.MISSING_CREATOR;
|
return ValidationResult.MISSING_CREATOR;
|
||||||
@ -553,6 +560,12 @@ public abstract class Transaction {
|
|||||||
if (countUnconfirmedByCreator(creator) >= Settings.getInstance().getMaxUnconfirmedPerAccount())
|
if (countUnconfirmedByCreator(creator) >= Settings.getInstance().getMaxUnconfirmedPerAccount())
|
||||||
return ValidationResult.TOO_MANY_UNCONFIRMED;
|
return ValidationResult.TOO_MANY_UNCONFIRMED;
|
||||||
|
|
||||||
|
// Transactions with a expiry prior to latest block's timestamp are too old
|
||||||
|
// Not relevant for lite nodes, as they don't have any blocks
|
||||||
|
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||||
|
if (this.getDeadline() <= latestBlock.getTimestamp())
|
||||||
|
return ValidationResult.TIMESTAMP_TOO_OLD;
|
||||||
|
|
||||||
// Check transaction's txGroupId
|
// Check transaction's txGroupId
|
||||||
if (!this.isValidTxGroupId())
|
if (!this.isValidTxGroupId())
|
||||||
return ValidationResult.INVALID_TX_GROUP_ID;
|
return ValidationResult.INVALID_TX_GROUP_ID;
|
||||||
|
@ -4,8 +4,12 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import org.qortal.account.NullAccount;
|
||||||
import org.qortal.data.transaction.ATTransactionData;
|
import org.qortal.data.transaction.ATTransactionData;
|
||||||
|
import org.qortal.data.transaction.BaseTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
|
import org.qortal.group.Group;
|
||||||
|
import org.qortal.transaction.Transaction;
|
||||||
import org.qortal.transform.TransformationException;
|
import org.qortal.transform.TransformationException;
|
||||||
import org.qortal.utils.Serialization;
|
import org.qortal.utils.Serialization;
|
||||||
|
|
||||||
@ -17,12 +21,97 @@ public class AtTransactionTransformer extends TransactionTransformer {
|
|||||||
protected static final TransactionLayout layout = null;
|
protected static final TransactionLayout layout = null;
|
||||||
|
|
||||||
// Property lengths
|
// Property lengths
|
||||||
|
|
||||||
|
private static final int MESSAGE_SIZE_LENGTH = INT_LENGTH;
|
||||||
|
private static final int TYPE_LENGTH = INT_LENGTH;
|
||||||
|
|
||||||
|
|
||||||
public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
|
public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
|
||||||
throw new TransformationException("Serialized AT transactions should not exist!");
|
long timestamp = byteBuffer.getLong();
|
||||||
|
|
||||||
|
int version = Transaction.getVersionByTimestamp(timestamp);
|
||||||
|
|
||||||
|
byte[] reference = new byte[REFERENCE_LENGTH];
|
||||||
|
byteBuffer.get(reference);
|
||||||
|
|
||||||
|
String atAddress = Serialization.deserializeAddress(byteBuffer);
|
||||||
|
|
||||||
|
String recipient = Serialization.deserializeAddress(byteBuffer);
|
||||||
|
|
||||||
|
// Default to PAYMENT-type, as there were no MESSAGE-type transactions before transaction v6
|
||||||
|
boolean isMessageType = false;
|
||||||
|
|
||||||
|
if (version >= 6) {
|
||||||
|
// Version 6 supports both PAYMENT-type and MESSAGE-type, specified using an integer.
|
||||||
|
// This could be extended to support additional types at a later date, simply by adding
|
||||||
|
// additional integer values.
|
||||||
|
int type = byteBuffer.getInt();
|
||||||
|
isMessageType = (type == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int messageLength = 0;
|
||||||
|
byte[] message = null;
|
||||||
|
long assetId = 0L;
|
||||||
|
long amount = 0L;
|
||||||
|
|
||||||
|
if (isMessageType) {
|
||||||
|
messageLength = byteBuffer.getInt();
|
||||||
|
|
||||||
|
message = new byte[messageLength];
|
||||||
|
byteBuffer.get(message);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Assume PAYMENT-type, as there were no MESSAGE-type transactions until this time
|
||||||
|
assetId = byteBuffer.getLong();
|
||||||
|
|
||||||
|
amount = byteBuffer.getLong();
|
||||||
|
}
|
||||||
|
|
||||||
|
long fee = byteBuffer.getLong();
|
||||||
|
|
||||||
|
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||||
|
byteBuffer.get(signature);
|
||||||
|
|
||||||
|
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, NullAccount.PUBLIC_KEY, fee, signature);
|
||||||
|
|
||||||
|
if (isMessageType) {
|
||||||
|
// MESSAGE-type
|
||||||
|
return new ATTransactionData(baseTransactionData, atAddress, recipient, message);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// PAYMENT-type
|
||||||
|
return new ATTransactionData(baseTransactionData, atAddress, recipient, amount, assetId);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getDataLength(TransactionData transactionData) throws TransformationException {
|
public static int getDataLength(TransactionData transactionData) throws TransformationException {
|
||||||
throw new TransformationException("Serialized AT transactions should not exist!");
|
ATTransactionData atTransactionData = (ATTransactionData) transactionData;
|
||||||
|
int version = Transaction.getVersionByTimestamp(transactionData.getTimestamp());
|
||||||
|
|
||||||
|
final int baseLength = TYPE_LENGTH + TIMESTAMP_LENGTH + REFERENCE_LENGTH + ADDRESS_LENGTH + ADDRESS_LENGTH +
|
||||||
|
FEE_LENGTH + SIGNATURE_LENGTH;
|
||||||
|
|
||||||
|
int typeSpecificLength = 0;
|
||||||
|
|
||||||
|
byte[] message = atTransactionData.getMessage();
|
||||||
|
boolean isMessageType = (message != null);
|
||||||
|
|
||||||
|
// MESSAGE-type and PAYMENT-type transactions will have differing lengths
|
||||||
|
if (isMessageType) {
|
||||||
|
typeSpecificLength = MESSAGE_SIZE_LENGTH + message.length;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
typeSpecificLength = ASSET_ID_LENGTH + AMOUNT_LENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
// V6 transactions include an extra integer to denote the type
|
||||||
|
int versionSpecificLength = 0;
|
||||||
|
if (version >= 6) {
|
||||||
|
versionSpecificLength = TYPE_LENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseLength + typeSpecificLength + versionSpecificLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used for generating fake transaction signatures
|
// Used for generating fake transaction signatures
|
||||||
@ -30,6 +119,8 @@ public class AtTransactionTransformer extends TransactionTransformer {
|
|||||||
try {
|
try {
|
||||||
ATTransactionData atTransactionData = (ATTransactionData) transactionData;
|
ATTransactionData atTransactionData = (ATTransactionData) transactionData;
|
||||||
|
|
||||||
|
int version = Transaction.getVersionByTimestamp(atTransactionData.getTimestamp());
|
||||||
|
|
||||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
bytes.write(Ints.toByteArray(atTransactionData.getType().value));
|
bytes.write(Ints.toByteArray(atTransactionData.getType().value));
|
||||||
@ -42,7 +133,17 @@ public class AtTransactionTransformer extends TransactionTransformer {
|
|||||||
|
|
||||||
byte[] message = atTransactionData.getMessage();
|
byte[] message = atTransactionData.getMessage();
|
||||||
|
|
||||||
if (message != null) {
|
boolean isMessageType = (message != null);
|
||||||
|
int type = isMessageType ? 1 : 0;
|
||||||
|
|
||||||
|
if (version >= 6) {
|
||||||
|
// Version 6 supports both PAYMENT-type and MESSAGE-type, specified using an integer.
|
||||||
|
// This could be extended to support additional types at a later date, simply by adding
|
||||||
|
// additional integer values.
|
||||||
|
bytes.write(Ints.toByteArray(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMessageType) {
|
||||||
// MESSAGE-type
|
// MESSAGE-type
|
||||||
bytes.write(Ints.toByteArray(message.length));
|
bytes.write(Ints.toByteArray(message.length));
|
||||||
bytes.write(message);
|
bytes.write(message);
|
||||||
|
@ -58,7 +58,8 @@
|
|||||||
"newBlockSigHeight": 320000,
|
"newBlockSigHeight": 320000,
|
||||||
"shareBinFix": 399000,
|
"shareBinFix": 399000,
|
||||||
"calcChainWeightTimestamp": 1620579600000,
|
"calcChainWeightTimestamp": 1620579600000,
|
||||||
"transactionV5Timestamp": 1642176000000
|
"transactionV5Timestamp": 1642176000000,
|
||||||
|
"transactionV6Timestamp": 9999999999999
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -27,6 +27,8 @@ DB_MAINTENANCE = Datenbank Instandhaltung
|
|||||||
|
|
||||||
EXIT = Verlassen
|
EXIT = Verlassen
|
||||||
|
|
||||||
|
LITE_NODE = Lite node
|
||||||
|
|
||||||
MINTING_DISABLED = NOT minting
|
MINTING_DISABLED = NOT minting
|
||||||
|
|
||||||
MINTING_ENABLED = \u2714 Minting
|
MINTING_ENABLED = \u2714 Minting
|
||||||
|
@ -27,6 +27,8 @@ DB_MAINTENANCE = Database Maintenance
|
|||||||
|
|
||||||
EXIT = Exit
|
EXIT = Exit
|
||||||
|
|
||||||
|
LITE_NODE = Lite node
|
||||||
|
|
||||||
MINTING_DISABLED = NOT minting
|
MINTING_DISABLED = NOT minting
|
||||||
|
|
||||||
MINTING_ENABLED = \u2714 Minting
|
MINTING_ENABLED = \u2714 Minting
|
||||||
|
@ -27,6 +27,8 @@ DB_MAINTENANCE = Tietokannan ylläpito
|
|||||||
|
|
||||||
EXIT = Pois
|
EXIT = Pois
|
||||||
|
|
||||||
|
LITE_NODE = Lite node
|
||||||
|
|
||||||
MINTING_DISABLED = EI lyö rahaa
|
MINTING_DISABLED = EI lyö rahaa
|
||||||
|
|
||||||
MINTING_ENABLED = \u2714 Lyö rahaa
|
MINTING_ENABLED = \u2714 Lyö rahaa
|
||||||
|
@ -27,6 +27,8 @@ DB_MAINTENANCE = Maintenance de la base de données
|
|||||||
|
|
||||||
EXIT = Quitter
|
EXIT = Quitter
|
||||||
|
|
||||||
|
LITE_NODE = Lite node
|
||||||
|
|
||||||
MINTING_DISABLED = NE mint PAS
|
MINTING_DISABLED = NE mint PAS
|
||||||
|
|
||||||
MINTING_ENABLED = \u2714 Minting
|
MINTING_ENABLED = \u2714 Minting
|
||||||
|
@ -27,6 +27,8 @@ DB_MAINTENANCE = Adatbázis karbantartás
|
|||||||
|
|
||||||
EXIT = Kilépés
|
EXIT = Kilépés
|
||||||
|
|
||||||
|
LITE_NODE = Lite node
|
||||||
|
|
||||||
MINTING_DISABLED = QORT-érmeverés jelenleg nincs folyamatban
|
MINTING_DISABLED = QORT-érmeverés jelenleg nincs folyamatban
|
||||||
|
|
||||||
MINTING_ENABLED = \u2714 QORT-érmeverés folyamatban
|
MINTING_ENABLED = \u2714 QORT-érmeverés folyamatban
|
||||||
|
@ -27,6 +27,8 @@ DB_MAINTENANCE = Manutenzione del database
|
|||||||
|
|
||||||
EXIT = Uscita
|
EXIT = Uscita
|
||||||
|
|
||||||
|
LITE_NODE = Lite node
|
||||||
|
|
||||||
MINTING_DISABLED = Conio disabilitato
|
MINTING_DISABLED = Conio disabilitato
|
||||||
|
|
||||||
MINTING_ENABLED = \u2714 Conio abilitato
|
MINTING_ENABLED = \u2714 Conio abilitato
|
||||||
|
@ -27,6 +27,8 @@ DB_MAINTENANCE = Database Onderhoud
|
|||||||
|
|
||||||
EXIT = Verlaten
|
EXIT = Verlaten
|
||||||
|
|
||||||
|
LITE_NODE = Lite node
|
||||||
|
|
||||||
MINTING_DISABLED = Minten is uitgeschakeld
|
MINTING_DISABLED = Minten is uitgeschakeld
|
||||||
|
|
||||||
MINTING_ENABLED = \u2714 Minten is ingeschakeld
|
MINTING_ENABLED = \u2714 Minten is ingeschakeld
|
||||||
|
@ -27,6 +27,8 @@ DB_MAINTENANCE = Обслуживание базы данных
|
|||||||
|
|
||||||
EXIT = Выход
|
EXIT = Выход
|
||||||
|
|
||||||
|
LITE_NODE = Lite node
|
||||||
|
|
||||||
MINTING_DISABLED = Чеканка отключена
|
MINTING_DISABLED = Чеканка отключена
|
||||||
|
|
||||||
MINTING_ENABLED = \u2714 Чеканка активна
|
MINTING_ENABLED = \u2714 Чеканка активна
|
||||||
|
@ -27,6 +27,8 @@ DB_MAINTENANCE = 数据库维护
|
|||||||
|
|
||||||
EXIT = 退出核心
|
EXIT = 退出核心
|
||||||
|
|
||||||
|
LITE_NODE = Lite node
|
||||||
|
|
||||||
MINTING_DISABLED = 没有铸币
|
MINTING_DISABLED = 没有铸币
|
||||||
|
|
||||||
MINTING_ENABLED = \u2714 铸币
|
MINTING_ENABLED = \u2714 铸币
|
||||||
|
@ -27,6 +27,8 @@ DB_MAINTENANCE = 數據庫維護
|
|||||||
|
|
||||||
EXIT = 退出核心
|
EXIT = 退出核心
|
||||||
|
|
||||||
|
LITE_NODE = Lite node
|
||||||
|
|
||||||
MINTING_DISABLED = 沒有鑄幣
|
MINTING_DISABLED = 沒有鑄幣
|
||||||
|
|
||||||
MINTING_ENABLED = \u2714 鑄幣
|
MINTING_ENABLED = \u2714 鑄幣
|
||||||
|
@ -47,7 +47,6 @@ public class SerializationTests extends Common {
|
|||||||
switch (txType) {
|
switch (txType) {
|
||||||
case GENESIS:
|
case GENESIS:
|
||||||
case ACCOUNT_FLAGS:
|
case ACCOUNT_FLAGS:
|
||||||
case AT:
|
|
||||||
case CHAT:
|
case CHAT:
|
||||||
case PUBLICIZE:
|
case PUBLICIZE:
|
||||||
case AIRDROP:
|
case AIRDROP:
|
||||||
|
@ -15,7 +15,7 @@ public class CheckTranslations {
|
|||||||
private static final String[] SUPPORTED_LANGS = new String[] { "en", "de", "zh", "ru" };
|
private static final String[] SUPPORTED_LANGS = new String[] { "en", "de", "zh", "ru" };
|
||||||
private static final Set<String> SYSTRAY_KEYS = Set.of("AUTO_UPDATE", "APPLYING_UPDATE_AND_RESTARTING", "BLOCK_HEIGHT",
|
private static final Set<String> SYSTRAY_KEYS = Set.of("AUTO_UPDATE", "APPLYING_UPDATE_AND_RESTARTING", "BLOCK_HEIGHT",
|
||||||
"BUILD_VERSION", "CHECK_TIME_ACCURACY", "CONNECTING", "CONNECTION", "CONNECTIONS", "CREATING_BACKUP_OF_DB_FILES",
|
"BUILD_VERSION", "CHECK_TIME_ACCURACY", "CONNECTING", "CONNECTION", "CONNECTIONS", "CREATING_BACKUP_OF_DB_FILES",
|
||||||
"DB_BACKUP", "DB_CHECKPOINT", "EXIT", "MINTING_DISABLED", "MINTING_ENABLED", "OPEN_UI", "PERFORMING_DB_CHECKPOINT",
|
"DB_BACKUP", "DB_CHECKPOINT", "EXIT", "LITE_NODE", "MINTING_DISABLED", "MINTING_ENABLED", "OPEN_UI", "PERFORMING_DB_CHECKPOINT",
|
||||||
"SYNCHRONIZE_CLOCK", "SYNCHRONIZING_BLOCKCHAIN", "SYNCHRONIZING_CLOCK");
|
"SYNCHRONIZE_CLOCK", "SYNCHRONIZING_BLOCKCHAIN", "SYNCHRONIZING_CLOCK");
|
||||||
|
|
||||||
private static String failurePrefix;
|
private static String failurePrefix;
|
||||||
|
96
src/test/java/org/qortal/test/at/AtSerializationTests.java
Normal file
96
src/test/java/org/qortal/test/at/AtSerializationTests.java
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package org.qortal.test.at;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
|
import org.qortal.data.transaction.ATTransactionData;
|
||||||
|
import org.qortal.data.transaction.TransactionData;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
import org.qortal.test.common.Common;
|
||||||
|
import org.qortal.test.common.transaction.AtTestTransaction;
|
||||||
|
import org.qortal.transaction.Transaction;
|
||||||
|
import org.qortal.transform.TransformationException;
|
||||||
|
import org.qortal.transform.transaction.TransactionTransformer;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
public class AtSerializationTests extends Common {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void beforeTest() throws DataException {
|
||||||
|
Common.useDefaultSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void afterTest() throws DataException {
|
||||||
|
Common.orphanCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPaymentTypeAtSerialization() throws DataException, TransformationException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
// Build PAYMENT-type AT transaction
|
||||||
|
PrivateKeyAccount signingAccount = Common.getTestAccount(repository, "alice");
|
||||||
|
ATTransactionData transactionData = (ATTransactionData) AtTestTransaction.paymentType(repository, signingAccount, true);
|
||||||
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
|
transaction.sign(signingAccount);
|
||||||
|
|
||||||
|
final int claimedLength = TransactionTransformer.getDataLength(transactionData);
|
||||||
|
byte[] serializedTransaction = TransactionTransformer.toBytes(transactionData);
|
||||||
|
assertEquals("Serialized PAYMENT-type AT transaction length differs from declared length", claimedLength, serializedTransaction.length);
|
||||||
|
|
||||||
|
TransactionData deserializedTransactionData = TransactionTransformer.fromBytes(serializedTransaction);
|
||||||
|
// Re-sign
|
||||||
|
Transaction deserializedTransaction = Transaction.fromData(repository, deserializedTransactionData);
|
||||||
|
deserializedTransaction.sign(signingAccount);
|
||||||
|
assertEquals("Deserialized PAYMENT-type AT transaction signature differs", Base58.encode(transactionData.getSignature()), Base58.encode(deserializedTransactionData.getSignature()));
|
||||||
|
|
||||||
|
// Re-serialize to check new length and bytes
|
||||||
|
final int reclaimedLength = TransactionTransformer.getDataLength(deserializedTransactionData);
|
||||||
|
assertEquals("Reserialized PAYMENT-type AT transaction declared length differs", claimedLength, reclaimedLength);
|
||||||
|
|
||||||
|
byte[] reserializedTransaction = TransactionTransformer.toBytes(deserializedTransactionData);
|
||||||
|
assertEquals("Reserialized PAYMENT-type AT transaction bytes differ", HashCode.fromBytes(serializedTransaction).toString(), HashCode.fromBytes(reserializedTransaction).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMessageTypeAtSerialization() throws DataException, TransformationException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
// Build MESSAGE-type AT transaction
|
||||||
|
PrivateKeyAccount signingAccount = Common.getTestAccount(repository, "alice");
|
||||||
|
ATTransactionData transactionData = (ATTransactionData) AtTestTransaction.messageType(repository, signingAccount, true);
|
||||||
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
|
transaction.sign(signingAccount);
|
||||||
|
|
||||||
|
// MESSAGE-type AT transactions are only fully supported since transaction V6
|
||||||
|
assertEquals(6, Transaction.getVersionByTimestamp(transactionData.getTimestamp()));
|
||||||
|
|
||||||
|
final int claimedLength = TransactionTransformer.getDataLength(transactionData);
|
||||||
|
byte[] serializedTransaction = TransactionTransformer.toBytes(transactionData);
|
||||||
|
assertEquals("Serialized MESSAGE-type AT transaction length differs from declared length", claimedLength, serializedTransaction.length);
|
||||||
|
|
||||||
|
TransactionData deserializedTransactionData = TransactionTransformer.fromBytes(serializedTransaction);
|
||||||
|
// Re-sign
|
||||||
|
Transaction deserializedTransaction = Transaction.fromData(repository, deserializedTransactionData);
|
||||||
|
deserializedTransaction.sign(signingAccount);
|
||||||
|
assertEquals("Deserialized MESSAGE-type AT transaction signature differs", Base58.encode(transactionData.getSignature()), Base58.encode(deserializedTransactionData.getSignature()));
|
||||||
|
|
||||||
|
// Re-serialize to check new length and bytes
|
||||||
|
final int reclaimedLength = TransactionTransformer.getDataLength(deserializedTransactionData);
|
||||||
|
assertEquals("Reserialized MESSAGE-type AT transaction declared length differs", claimedLength, reclaimedLength);
|
||||||
|
|
||||||
|
byte[] reserializedTransaction = TransactionTransformer.toBytes(deserializedTransactionData);
|
||||||
|
assertEquals("Reserialized MESSAGE-type AT transaction bytes differ", HashCode.fromBytes(serializedTransaction).toString(), HashCode.fromBytes(reserializedTransaction).toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -12,16 +12,33 @@ import org.qortal.utils.Amounts;
|
|||||||
public class AtTestTransaction extends TestTransaction {
|
public class AtTestTransaction extends TestTransaction {
|
||||||
|
|
||||||
public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException {
|
public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException {
|
||||||
|
return AtTestTransaction.paymentType(repository, account, wantValid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TransactionData paymentType(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException {
|
||||||
byte[] signature = new byte[64];
|
byte[] signature = new byte[64];
|
||||||
random.nextBytes(signature);
|
random.nextBytes(signature);
|
||||||
String atAddress = Crypto.toATAddress(signature);
|
String atAddress = Crypto.toATAddress(signature);
|
||||||
String recipient = account.getAddress();
|
String recipient = account.getAddress();
|
||||||
|
|
||||||
|
// Use PAYMENT-type
|
||||||
long amount = 123L * Amounts.MULTIPLIER;
|
long amount = 123L * Amounts.MULTIPLIER;
|
||||||
final long assetId = Asset.QORT;
|
final long assetId = Asset.QORT;
|
||||||
|
|
||||||
|
return new ATTransactionData(generateBase(account), atAddress, recipient, amount, assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TransactionData messageType(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException {
|
||||||
|
byte[] signature = new byte[64];
|
||||||
|
random.nextBytes(signature);
|
||||||
|
String atAddress = Crypto.toATAddress(signature);
|
||||||
|
String recipient = account.getAddress();
|
||||||
|
|
||||||
|
// Use MESSAGE-type
|
||||||
byte[] message = new byte[32];
|
byte[] message = new byte[32];
|
||||||
random.nextBytes(message);
|
random.nextBytes(message);
|
||||||
|
|
||||||
return new ATTransactionData(generateBase(account), atAddress, recipient, amount, assetId, message);
|
return new ATTransactionData(generateBase(account), atAddress, recipient, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,8 @@
|
|||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0,
|
"calcChainWeightTimestamp": 0,
|
||||||
"transactionV5Timestamp": 0
|
"transactionV5Timestamp": 0,
|
||||||
|
"transactionV6Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -52,7 +52,8 @@
|
|||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0,
|
"calcChainWeightTimestamp": 0,
|
||||||
"transactionV5Timestamp": 0
|
"transactionV5Timestamp": 0,
|
||||||
|
"transactionV6Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -52,7 +52,8 @@
|
|||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0,
|
"calcChainWeightTimestamp": 0,
|
||||||
"transactionV5Timestamp": 0
|
"transactionV5Timestamp": 0,
|
||||||
|
"transactionV6Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -52,7 +52,8 @@
|
|||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0,
|
"calcChainWeightTimestamp": 0,
|
||||||
"transactionV5Timestamp": 0
|
"transactionV5Timestamp": 0,
|
||||||
|
"transactionV6Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -52,7 +52,8 @@
|
|||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0,
|
"calcChainWeightTimestamp": 0,
|
||||||
"transactionV5Timestamp": 0
|
"transactionV5Timestamp": 0,
|
||||||
|
"transactionV6Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -52,7 +52,8 @@
|
|||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 6,
|
"shareBinFix": 6,
|
||||||
"calcChainWeightTimestamp": 0,
|
"calcChainWeightTimestamp": 0,
|
||||||
"transactionV5Timestamp": 0
|
"transactionV5Timestamp": 0,
|
||||||
|
"transactionV6Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -52,7 +52,8 @@
|
|||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0,
|
"calcChainWeightTimestamp": 0,
|
||||||
"transactionV5Timestamp": 0
|
"transactionV5Timestamp": 0,
|
||||||
|
"transactionV6Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -52,7 +52,8 @@
|
|||||||
"newBlockSigHeight": 999999,
|
"newBlockSigHeight": 999999,
|
||||||
"shareBinFix": 999999,
|
"shareBinFix": 999999,
|
||||||
"calcChainWeightTimestamp": 0,
|
"calcChainWeightTimestamp": 0,
|
||||||
"transactionV5Timestamp": 0
|
"transactionV5Timestamp": 0,
|
||||||
|
"transactionV6Timestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
Loading…
Reference in New Issue
Block a user