Interim commit: online accounts and account levels

Safety commit in case of data loss!

Lots of changes to do with "online accounts", including:

* networking
	+ GET_ONLINE_ACCOUNTS * ONLINE_ACCOUNTS messages & handling
	+ Changes to serialization of block data to include online accounts info
* block-related
	+ Adding online accounts info when generating blocks
	+ Validating online accounts info in block data
	+ Repository changes to store online accounts info
* Controller
	+ Managing in-memory cache of online accounts
	+ Updating/broadcasting our online accounts
* BlockChain config

Added "account levels", so new code/changes required in the usual places, like:

* transaction data object
* repository
* transaction transformer
* transaction processing
This commit is contained in:
catbref 2019-09-13 14:21:04 +01:00
parent 2cc926666b
commit 504cfc6a74
30 changed files with 1781 additions and 177 deletions

View File

@ -250,4 +250,16 @@ public class Account {
this.repository.getAccountRepository().setForgingEnabler(accountData);
}
// Account level
public Integer getLevel() throws DataException {
return this.repository.getAccountRepository().getLevel(this.address);
}
public void setLevel(int level) throws DataException {
AccountData accountData = this.buildAccountData();
accountData.setLevel(level);
this.repository.getAccountRepository().setLevel(accountData);
}
}

View File

@ -30,9 +30,11 @@ import org.qora.api.ApiExceptionFactory;
import org.qora.api.model.ProxyKeyRequest;
import org.qora.api.resource.TransactionsResource;
import org.qora.asset.Asset;
import org.qora.controller.Controller;
import org.qora.crypto.Crypto;
import org.qora.data.account.AccountData;
import org.qora.data.account.ProxyForgerData;
import org.qora.data.network.OnlineAccount;
import org.qora.data.transaction.ProxyForgingTransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
@ -150,6 +152,21 @@ public class AddressesResource {
return Crypto.isValidAddress(address);
}
@GET
@Path("/online")
@Operation(
summary = "Return currently 'online' accounts",
responses = {
@ApiResponse(
description = "online accounts",
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = OnlineAccount.class)))
)
}
)
public List<OnlineAccount> getOnlineAccounts() {
return Controller.getInstance().getOnlineAccounts();
}
@GET
@Path("/generatingbalance/{address}")
@Operation(

View File

@ -8,6 +8,7 @@ import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -20,12 +21,14 @@ import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.at.AT;
import org.qora.block.BlockChain;
import org.qora.controller.Controller;
import org.qora.crypto.Crypto;
import org.qora.data.account.ProxyForgerData;
import org.qora.data.at.ATData;
import org.qora.data.at.ATStateData;
import org.qora.data.block.BlockData;
import org.qora.data.block.BlockTransactionData;
import org.qora.data.network.OnlineAccount;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.ATRepository;
import org.qora.repository.BlockRepository;
@ -37,12 +40,17 @@ import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.ApprovalStatus;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.transform.TransformationException;
import org.qora.transform.Transformer;
import org.qora.transform.block.BlockTransformer;
import org.qora.transform.transaction.TransactionTransformer;
import org.qora.utils.Base58;
import org.qora.utils.NTP;
import org.roaringbitmap.IntIterator;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
import io.druid.extendedset.intset.ConciseSet;
/*
* Typical use-case scenarios:
@ -90,7 +98,12 @@ public class Block {
TRANSACTION_PROCESSING_FAILED(53),
TRANSACTION_ALREADY_PROCESSED(54),
TRANSACTION_NEEDS_APPROVAL(55),
AT_STATES_MISMATCH(61);
AT_STATES_MISMATCH(61),
ONLINE_ACCOUNT_LEVEL_ZERO(70),
ONLINE_ACCOUNT_UNKNOWN(71),
ONLINE_ACCOUNT_SIGNATURES_MISSING(72),
ONLINE_ACCOUNT_SIGNATURES_MALFORMED(73),
ONLINE_ACCOUNT_SIGNATURE_INCORRECT(74);
public final int value;
@ -134,6 +147,8 @@ public class Block {
// TODO push this out to blockchain config file
public static final int MAX_BLOCK_BYTES = 1048576;
public static final ConciseSet EMPTY_ONLINE_ACCOUNTS = new ConciseSet();
// Constructors
/**
@ -207,10 +222,48 @@ public class Block {
byte[] reference = parentBlockData.getSignature();
BigDecimal generatingBalance = parentBlock.calcNextBlockGeneratingBalance();
// Fetch our list of online accounts
List<OnlineAccount> onlineAccounts = Controller.getInstance().getOnlineAccounts();
if (onlineAccounts.isEmpty())
throw new IllegalStateException("No online accounts - not even our own?");
// Find newest online accounts timestamp
long onlineAccountsTimestamp = 0;
for (OnlineAccount onlineAccount : onlineAccounts) {
if (onlineAccount.getTimestamp() > onlineAccountsTimestamp)
onlineAccountsTimestamp = onlineAccount.getTimestamp();
}
// Map using account index (in list of proxy forger accounts)
Map<Integer, OnlineAccount> indexedOnlineAccounts = new HashMap<>();
for (OnlineAccount onlineAccount : onlineAccounts) {
// Disregard online accounts with different timestamps
if (onlineAccount.getTimestamp() != onlineAccountsTimestamp)
continue;
int accountIndex = repository.getAccountRepository().getProxyAccountIndex(onlineAccount.getPublicKey());
indexedOnlineAccounts.put(accountIndex, onlineAccount);
}
List<Integer> accountIndexes = new ArrayList<>(indexedOnlineAccounts.keySet());
accountIndexes.sort(null);
// Convert to compressed integer set
ConciseSet onlineAccountsSet = new ConciseSet();
onlineAccountsSet = onlineAccountsSet.convert(accountIndexes);
byte[] encodedOnlineAccounts = BlockTransformer.encodeOnlineAccounts(onlineAccountsSet);
// Concatenate online account timestamp signatures (in correct order)
byte[] timestampSignatures = new byte[accountIndexes.size() * Transformer.SIGNATURE_LENGTH];
for (int i = 0; i < accountIndexes.size(); ++i) {
Integer accountIndex = accountIndexes.get(i);
OnlineAccount onlineAccount = indexedOnlineAccounts.get(accountIndex);
System.arraycopy(onlineAccount.getSignature(), 0, timestampSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH);
}
byte[] generatorSignature;
try {
generatorSignature = generator
.sign(BlockTransformer.getBytesForGeneratorSignature(parentBlockData.getGeneratorSignature(), generatingBalance, generator));
.sign(BlockTransformer.getBytesForGeneratorSignature(parentBlockData.getGeneratorSignature(), generatingBalance, generator, encodedOnlineAccounts));
} catch (TransformationException e) {
throw new DataException("Unable to calculate next block generator signature", e);
}
@ -229,7 +282,7 @@ public class Block {
// This instance used for AT processing
this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generator.getPublicKey(), generatorSignature, atCount, atFees);
generator.getPublicKey(), generatorSignature, atCount, atFees, encodedOnlineAccounts, onlineAccountsTimestamp, timestampSignatures);
// Requires this.blockData and this.transactions, sets this.ourAtStates and this.ourAtFees
this.executeATs();
@ -241,7 +294,7 @@ public class Block {
// Rebuild blockData using post-AT-execute data
this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generator.getPublicKey(), generatorSignature, atCount, atFees);
generator.getPublicKey(), generatorSignature, atCount, atFees, encodedOnlineAccounts, onlineAccountsTimestamp, timestampSignatures);
}
/**
@ -273,7 +326,7 @@ public class Block {
byte[] generatorSignature;
try {
generatorSignature = generator
.sign(BlockTransformer.getBytesForGeneratorSignature(parentBlockData.getGeneratorSignature(), generatingBalance, generator));
.sign(BlockTransformer.getBytesForGeneratorSignature(parentBlockData.getGeneratorSignature(), generatingBalance, generator, this.blockData.getEncodedOnlineAccounts()));
} catch (TransformationException e) {
throw new DataException("Unable to calculate next block generator signature", e);
}
@ -289,8 +342,12 @@ public class Block {
int atCount = newBlock.ourAtStates.size();
BigDecimal atFees = newBlock.ourAtFees;
byte[] encodedOnlineAccounts = this.blockData.getEncodedOnlineAccounts();
Long onlineAccountsTimestamp = this.blockData.getOnlineAccountsTimestamp();
byte[] timestampSignatures = this.blockData.getTimestampSignatures();
newBlock.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generator.getPublicKey(), generatorSignature, atCount, atFees);
generator.getPublicKey(), generatorSignature, atCount, atFees, encodedOnlineAccounts, onlineAccountsTimestamp, timestampSignatures);
// Resign to update transactions signature
newBlock.sign();
@ -811,6 +868,50 @@ public class Block {
return ValidationResult.OK;
}
public ValidationResult areOnlineAccountsValid() throws DataException {
// Expand block's online accounts indexes into actual accounts
ConciseSet accountIndexes = BlockTransformer.decodeOnlineAccounts(this.blockData.getEncodedOnlineAccounts());
List<ProxyForgerData> expandedAccounts = new ArrayList<>();
IntIterator iterator = accountIndexes.iterator();
while (iterator.hasNext()) {
int accountIndex = iterator.next();
ProxyForgerData proxyAccountData = repository.getAccountRepository().getProxyAccountByIndex(accountIndex);
// Check that claimed online account actually exists
if (proxyAccountData == null)
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
expandedAccounts.add(proxyAccountData);
}
// Possibly check signatures if block is recent
long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime();
if (this.blockData.getTimestamp() >= signatureRequirementThreshold) {
if (this.blockData.getTimestampSignatures() == null || this.blockData.getTimestampSignatures().length == 0)
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MISSING;
if (this.blockData.getTimestampSignatures().length != expandedAccounts.size() * Transformer.SIGNATURE_LENGTH)
return ValidationResult.ONLINE_ACCOUNT_SIGNATURES_MALFORMED;
// Check signatures
List<byte[]> timestampSignatures = BlockTransformer.decodeTimestampSignatures(this.blockData.getTimestampSignatures());
byte[] message = Longs.toByteArray(this.blockData.getOnlineAccountsTimestamp());
for (int i = 0; i < timestampSignatures.size(); ++i) {
PublicKeyAccount account = new PublicKeyAccount(null, expandedAccounts.get(i).getProxyPublicKey());
byte[] signature = timestampSignatures.get(i);
if (!account.verify(signature, message))
return ValidationResult.ONLINE_ACCOUNT_SIGNATURE_INCORRECT;
}
}
return ValidationResult.OK;
}
/**
* Returns whether Block is valid.
* <p>
@ -863,6 +964,11 @@ public class Block {
if (!isGeneratorValidToForge(parentBlock))
return ValidationResult.GENERATOR_NOT_ACCEPTED;
// Online Accounts
ValidationResult onlineAccountsResult = this.areOnlineAccountsValid();
if (onlineAccountsResult != ValidationResult.OK)
return onlineAccountsResult;
// CIYAM ATs
if (this.blockData.getATCount() != 0) {
// Locally generated AT states should be valid so no need to re-execute them

View File

@ -110,6 +110,12 @@ public class BlockChain {
private int maxProxyRelationships;
/** Minimum time to retain online account signatures (ms) for block validity checks. */
private long onlineAccountSignaturesMinLifetime;
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
private long onlineAccountSignaturesMaxLifetime;
// Constructors, etc.
private BlockChain() {
@ -266,6 +272,14 @@ public class BlockChain {
return this.maxProxyRelationships;
}
public long getOnlineAccountSignaturesMinLifetime() {
return this.onlineAccountSignaturesMinLifetime;
}
public long getOnlineAccountSignaturesMaxLifetime() {
return this.onlineAccountSignaturesMaxLifetime;
}
// Convenience methods for specific blockchain feature triggers
public long getMessageReleaseHeight() {

View File

@ -92,6 +92,10 @@ public class BlockGenerator extends Thread {
if (now == null)
continue;
// No online accounts? (e.g. during startup)
if (Controller.getInstance().getOnlineAccounts().isEmpty())
continue;
List<ForgingAccountData> forgingAccountsData = repository.getAccountRepository().getForgingAccounts();
// No forging accounts?
if (forgingAccountsData.isEmpty())

View File

@ -11,6 +11,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
@ -18,19 +19,24 @@ import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qora.account.PrivateKeyAccount;
import org.qora.account.PublicKeyAccount;
import org.qora.api.ApiService;
import org.qora.block.Block;
import org.qora.block.BlockChain;
import org.qora.block.BlockGenerator;
import org.qora.controller.Synchronizer.SynchronizationResult;
import org.qora.crypto.Crypto;
import org.qora.data.account.ForgingAccountData;
import org.qora.data.block.BlockData;
import org.qora.data.block.BlockSummaryData;
import org.qora.data.network.OnlineAccount;
import org.qora.data.network.PeerData;
import org.qora.data.transaction.ArbitraryTransactionData;
import org.qora.data.transaction.ArbitraryTransactionData.DataType;
@ -46,6 +52,7 @@ import org.qora.network.message.BlockSummariesMessage;
import org.qora.network.message.GetArbitraryDataMessage;
import org.qora.network.message.GetBlockMessage;
import org.qora.network.message.GetBlockSummariesMessage;
import org.qora.network.message.GetOnlineAccountsMessage;
import org.qora.network.message.GetPeersMessage;
import org.qora.network.message.GetSignaturesMessage;
import org.qora.network.message.GetSignaturesV2Message;
@ -54,6 +61,7 @@ import org.qora.network.message.GetUnconfirmedTransactionsMessage;
import org.qora.network.message.HeightMessage;
import org.qora.network.message.HeightV2Message;
import org.qora.network.message.Message;
import org.qora.network.message.OnlineAccountsMessage;
import org.qora.network.message.SignaturesMessage;
import org.qora.network.message.TransactionMessage;
import org.qora.network.message.TransactionSignaturesMessage;
@ -72,6 +80,8 @@ import org.qora.utils.Base58;
import org.qora.utils.NTP;
import org.qora.utils.Triple;
import com.google.common.primitives.Longs;
public class Controller extends Thread {
static {
@ -94,6 +104,11 @@ public class Controller extends Thread {
private static final long NTP_POST_SYNC_CHECK_PERIOD = 5 * 60 * 1000; // ms
private static final long DELETE_EXPIRED_INTERVAL = 5 * 60 * 1000; // ms
// To do with online accounts list
private static final long ONLINE_ACCOUNTS_TASKS_INTERVAL = 10 * 1000; // ms
private static final long ONLINE_TIMESTAMP_MODULUS = 5 * 60 * 1000;
private static final long LAST_SEEN_EXPIRY_PERIOD = (ONLINE_TIMESTAMP_MODULUS * 2) + (1 * 60 * 1000);
private static volatile boolean isStopping = false;
private static BlockGenerator blockGenerator = null;
private static volatile boolean requestSync = false;
@ -108,6 +123,8 @@ public class Controller extends Thread {
private long repositoryBackupTimestamp = startTime + REPOSITORY_BACKUP_PERIOD; // ms
private long ntpCheckTimestamp = startTime; // ms
private long deleteExpiredTimestamp = startTime + DELETE_EXPIRED_INTERVAL; // ms
private long onlineAccountsTasksTimestamp = startTime + ONLINE_ACCOUNTS_TASKS_INTERVAL; // ms
/** Whether BlockGenerator is allowed to generate blocks. Mostly determined by system clock accuracy. */
private volatile boolean isGenerationAllowed = false;
@ -138,6 +155,9 @@ public class Controller extends Thread {
/** Lock for only allowing one blockchain-modifying codepath at a time. e.g. synchronization or newly generated block. */
private final ReentrantLock blockchainLock = new ReentrantLock();
/** Cache of 'online accounts' */
List<OnlineAccount> onlineAccounts = new ArrayList<>();
// Constructors
private Controller() {
@ -198,6 +218,7 @@ public class Controller extends Thread {
return this.chainTip.get();
}
/** Cache new blockchain tip, and also wipe cache of online accounts. */
public void setChainTip(BlockData blockData) {
this.chainTip.set(blockData);
}
@ -374,6 +395,12 @@ public class Controller extends Thread {
requestSysTrayUpdate = false;
updateSysTray();
}
// Perform tasks to do with managing online accounts list
if (now >= onlineAccountsTasksTimestamp) {
onlineAccountsTasksTimestamp = now + ONLINE_ACCOUNTS_TASKS_INTERVAL;
performOnlineAccountsTasks();
}
}
} catch (InterruptedException e) {
// Fall-through to exit
@ -1132,6 +1159,53 @@ public class Controller extends Thread {
break;
}
case GET_ONLINE_ACCOUNTS: {
GetOnlineAccountsMessage getOnlineAccountsMessage = (GetOnlineAccountsMessage) message;
List<OnlineAccount> excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
// Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts
List<OnlineAccount> accountsToSend;
synchronized (this.onlineAccounts) {
accountsToSend = new ArrayList<>(this.onlineAccounts);
}
Iterator<OnlineAccount> iterator = accountsToSend.iterator();
SEND_ITERATOR:
while (iterator.hasNext()) {
OnlineAccount onlineAccount = iterator.next();
for (int i = 0; i < excludeAccounts.size(); ++i) {
OnlineAccount excludeAccount = excludeAccounts.get(i);
if (onlineAccount.getTimestamp() == excludeAccount.getTimestamp() && Arrays.equals(onlineAccount.getPublicKey(), excludeAccount.getPublicKey())) {
iterator.remove();
continue SEND_ITERATOR;
}
}
}
Message onlineAccountsMessage = new OnlineAccountsMessage(accountsToSend);
peer.sendMessage(onlineAccountsMessage);
LOGGER.trace(() -> String.format("Sent %d of our %d online accounts to %s", accountsToSend.size(), this.onlineAccounts.size(), peer));
break;
}
case ONLINE_ACCOUNTS: {
OnlineAccountsMessage onlineAccountsMessage = (OnlineAccountsMessage) message;
List<OnlineAccount> onlineAccounts = onlineAccountsMessage.getOnlineAccounts();
LOGGER.trace(() -> String.format("Received %d online accounts from %s", onlineAccounts.size(), peer));
for (OnlineAccount onlineAccount : onlineAccounts)
this.verifyAndAddAccount(onlineAccount);
break;
}
default:
LOGGER.debug(String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer));
break;
@ -1140,6 +1214,158 @@ public class Controller extends Thread {
// Utilities
private void verifyAndAddAccount(OnlineAccount onlineAccount) {
// We would check timestamp is 'recent' here
// Verify
byte[] data = Longs.toByteArray(onlineAccount.getTimestamp());
PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccount.getPublicKey());
if (!otherAccount.verify(onlineAccount.getSignature(), data)) {
LOGGER.trace(() -> String.format("Rejecting invalid online account %s", otherAccount.getAddress()));
return;
}
synchronized (this.onlineAccounts) {
OnlineAccount existingAccount = this.onlineAccounts.stream().filter(account -> Arrays.equals(account.getPublicKey(), onlineAccount.getPublicKey())).findFirst().orElse(null);
if (existingAccount != null) {
if (existingAccount.getTimestamp() < onlineAccount.getTimestamp()) {
this.onlineAccounts.remove(existingAccount);
LOGGER.trace(() -> String.format("Updated online account %s with timestamp %d (was %d)", otherAccount.getAddress(), onlineAccount.getTimestamp(), existingAccount.getTimestamp()));
} else {
LOGGER.trace(() -> String.format("Not updating existing online account %s", otherAccount.getAddress()));
return;
}
} else {
LOGGER.trace(() -> String.format("Added online account %s with timestamp %d", otherAccount.getAddress(), onlineAccount.getTimestamp()));
}
this.onlineAccounts.add(onlineAccount);
}
}
private void performOnlineAccountsTasks() {
final long now = System.currentTimeMillis();
// Expire old entries
final long cutoffThreshold = now - LAST_SEEN_EXPIRY_PERIOD;
synchronized (this.onlineAccounts) {
Iterator<OnlineAccount> iterator = this.onlineAccounts.iterator();
while (iterator.hasNext()) {
OnlineAccount onlineAccount = iterator.next();
if (onlineAccount.getTimestamp() < cutoffThreshold) {
iterator.remove();
LOGGER.trace(() -> {
PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccount.getPublicKey());
return String.format("Removed expired online account %s with timestamp %d", otherAccount.getAddress(), onlineAccount.getTimestamp());
});
}
}
}
// Request data from another peer
Message message;
synchronized (this.onlineAccounts) {
message = new GetOnlineAccountsMessage(this.onlineAccounts);
}
Network.getInstance().broadcast((peer) -> message);
// Refresh our onlineness?
sendOurOnlineAccountsInfo();
}
private void sendOurOnlineAccountsInfo() {
final Long now = NTP.getTime();
if (now == null)
return;
List<ForgingAccountData> forgingAccounts;
try (final Repository repository = RepositoryManager.getRepository()) {
forgingAccounts = repository.getAccountRepository().getForgingAccounts();
// We have no accounts, but don't reset timestamp
if (forgingAccounts.isEmpty())
return;
// Only proxy forging accounts allowed
Iterator<ForgingAccountData> iterator = forgingAccounts.iterator();
while (iterator.hasNext()) {
ForgingAccountData forgingAccountData = iterator.next();
if (!repository.getAccountRepository().isProxyPublicKey(forgingAccountData.getPublicKey()))
iterator.remove();
}
} catch (DataException e) {
LOGGER.warn("Repository issue trying to fetch forging accounts: " + e.getMessage());
return;
}
// 'current' timestamp
final long onlineAccountsTimestamp = Controller.toOnlineAccountTimestamp(now);
boolean hasInfoChanged = false;
byte[] timestampBytes = Longs.toByteArray(onlineAccountsTimestamp);
List<OnlineAccount> ourOnlineAccounts = new ArrayList<>();
FORGING_ACCOUNTS:
for (ForgingAccountData forgingAccountData : forgingAccounts) {
PrivateKeyAccount forgingAccount = new PrivateKeyAccount(null, forgingAccountData.getSeed());
byte[] signature = forgingAccount.sign(timestampBytes);
byte[] publicKey = forgingAccount.getPublicKey();
// Our account is online
OnlineAccount onlineAccount = new OnlineAccount(onlineAccountsTimestamp, signature, publicKey);
synchronized (this.onlineAccounts) {
Iterator<OnlineAccount> iterator = this.onlineAccounts.iterator();
while (iterator.hasNext()) {
OnlineAccount account = iterator.next();
if (Arrays.equals(account.getPublicKey(), forgingAccount.getPublicKey())) {
// If onlineAccount is already present, with same timestamp, then move on to next forgingAccount
if (account.getTimestamp() == onlineAccountsTimestamp)
continue FORGING_ACCOUNTS;
// If onlineAccount is already present, but with older timestamp, then remove it
iterator.remove();
break;
}
}
this.onlineAccounts.add(onlineAccount);
}
LOGGER.trace(() -> String.format("Added our online account %s with timestamp %d", forgingAccount.getAddress(), onlineAccountsTimestamp));
ourOnlineAccounts.add(onlineAccount);
hasInfoChanged = true;
}
if (!hasInfoChanged)
return;
Message message = new OnlineAccountsMessage(ourOnlineAccounts);
Network.getInstance().broadcast((peer) -> message);
LOGGER.trace(( )-> String.format("Broadcasted %d online account%s with timestamp %d", ourOnlineAccounts.size(), (ourOnlineAccounts.size() != 1 ? "s" : ""), onlineAccountsTimestamp));
}
public static long toOnlineAccountTimestamp(long timestamp) {
return (timestamp / ONLINE_TIMESTAMP_MODULUS) * ONLINE_TIMESTAMP_MODULUS;
}
public List<OnlineAccount> getOnlineAccounts() {
final long onlineTimestamp = Controller.toOnlineAccountTimestamp(NTP.getTime());
synchronized (this.onlineAccounts) {
return this.onlineAccounts.stream().filter(account -> account.getTimestamp() == onlineTimestamp).collect(Collectors.toList());
}
}
public byte[] fetchArbitraryData(byte[] signature) throws InterruptedException {
// Build request
Message getArbitraryDataMessage = new GetArbitraryDataMessage(signature);

View File

@ -16,6 +16,7 @@ public class AccountData {
protected int defaultGroupId;
protected int flags;
protected String forgingEnabler;
protected int level;
// Constructors
@ -23,17 +24,18 @@ public class AccountData {
protected AccountData() {
}
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, String forgingEnabler) {
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, String forgingEnabler, int level) {
this.address = address;
this.reference = reference;
this.publicKey = publicKey;
this.defaultGroupId = defaultGroupId;
this.flags = flags;
this.forgingEnabler = forgingEnabler;
this.level = level;
}
public AccountData(String address) {
this(address, null, null, Group.NO_GROUP, 0, null);
this(address, null, null, Group.NO_GROUP, 0, null, 0);
}
// Getters/Setters
@ -82,6 +84,14 @@ public class AccountData {
this.forgingEnabler = forgingEnabler;
}
public int getLevel() {
return this.level;
}
public void setLevel(int level) {
this.level = level;
}
// Comparison
@Override

View File

@ -32,6 +32,9 @@ public class BlockData implements Serializable {
private byte[] generatorSignature;
private int atCount;
private BigDecimal atFees;
private byte[] encodedOnlineAccounts;
private Long onlineAccountsTimestamp;
private byte[] timestampSignatures;
// Constructors
@ -40,7 +43,8 @@ public class BlockData implements Serializable {
}
public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp,
BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, int atCount, BigDecimal atFees) {
BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, int atCount, BigDecimal atFees,
byte[] encodedOnlineAccounts, Long onlineAccountsTimestamp, byte[] timestampSignatures) {
this.version = version;
this.reference = reference;
this.transactionCount = transactionCount;
@ -53,6 +57,9 @@ public class BlockData implements Serializable {
this.generatorSignature = generatorSignature;
this.atCount = atCount;
this.atFees = atFees;
this.encodedOnlineAccounts = encodedOnlineAccounts;
this.onlineAccountsTimestamp = onlineAccountsTimestamp;
this.timestampSignatures = timestampSignatures;
if (this.generatorSignature != null && this.transactionsSignature != null)
this.signature = Bytes.concat(this.generatorSignature, this.transactionsSignature);
@ -60,6 +67,12 @@ public class BlockData implements Serializable {
this.signature = null;
}
public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp,
BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, int atCount, BigDecimal atFees) {
this(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, generatorPublicKey, generatorSignature, atCount, atFees,
null, null, null);
}
// Getters/setters
public byte[] getSignature() {
@ -150,6 +163,22 @@ public class BlockData implements Serializable {
this.atFees = atFees;
}
public byte[] getEncodedOnlineAccounts() {
return this.encodedOnlineAccounts;
}
public Long getOnlineAccountsTimestamp() {
return onlineAccountsTimestamp;
}
public void setOnlineAccountsTimestamp(Long onlineAccountsTimestamp) {
this.onlineAccountsTimestamp = onlineAccountsTimestamp;
}
public byte[] getTimestampSignatures() {
return this.timestampSignatures;
}
// JAXB special
@XmlElement(name = "generatorAddress")

View File

@ -0,0 +1,47 @@
package org.qora.data.network;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import org.qora.account.PublicKeyAccount;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class OnlineAccount {
protected long timestamp;
protected byte[] signature;
protected byte[] publicKey;
// Constructors
// necessary for JAXB serialization
protected OnlineAccount() {
}
public OnlineAccount(long timestamp, byte[] signature, byte[] publicKey) {
this.timestamp = timestamp;
this.signature = signature;
this.publicKey = publicKey;
}
public long getTimestamp() {
return this.timestamp;
}
public byte[] getSignature() {
return this.signature;
}
public byte[] getPublicKey() {
return this.publicKey;
}
// For JAXB
@XmlElement(name = "address")
protected String getAddress() {
return new PublicKeyAccount(null, this.publicKey).getAddress();
}
}

View File

@ -0,0 +1,91 @@
package org.qora.data.transaction;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;
import org.qora.account.GenesisAccount;
import org.qora.block.GenesisBlock;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = {TransactionData.class})
// JAXB: use this subclass if XmlDiscriminatorNode matches XmlDiscriminatorValue below:
@XmlDiscriminatorValue("ACCOUNT_LEVEL")
public class AccountLevelTransactionData extends TransactionData {
private String target;
private int level;
private Integer previousLevel;
// Constructors
// For JAXB
protected AccountLevelTransactionData() {
super(TransactionType.ACCOUNT_LEVEL);
}
public void afterUnmarshal(Unmarshaller u, Object parent) {
/*
* If we're being constructed as part of the genesis block info inside blockchain config
* and no specific creator's public key is supplied
* then use genesis account's public key.
*/
if (parent instanceof GenesisBlock.GenesisInfo && this.creatorPublicKey == null)
this.creatorPublicKey = GenesisAccount.PUBLIC_KEY;
}
/** From repository */
public AccountLevelTransactionData(BaseTransactionData baseTransactionData,
String target, int level, Integer previousLevel) {
super(TransactionType.ACCOUNT_LEVEL, baseTransactionData);
this.target = target;
this.level = level;
this.previousLevel = previousLevel;
}
/** From network/API */
public AccountLevelTransactionData(BaseTransactionData baseTransactionData,
String target, int level) {
this(baseTransactionData, target, level, null);
}
// Getters / setters
public String getTarget() {
return this.target;
}
public int getLevel() {
return this.level;
}
public Integer getPreviousLevel() {
return this.previousLevel;
}
public void setPreviousLevel(Integer previousLevel) {
this.previousLevel = previousLevel;
}
// Re-expose to JAXB
@XmlElement(name = "creatorPublicKey")
@Schema(name = "creatorPublicKey", description = "creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
public byte[] getAccountLevelCreatorPublicKey() {
return super.getCreatorPublicKey();
}
@XmlElement(name = "creatorPublicKey")
@Schema(name = "creatorPublicKey", description = "creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
public void setAccountLevelCreatorPublicKey(byte[] creatorPublicKey) {
super.setCreatorPublicKey(creatorPublicKey);
}
}

View File

@ -7,6 +7,7 @@ import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlTransient;
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;
import org.qora.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
@ -14,6 +15,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = {TransactionData.class})
// JAXB: use this subclass if XmlDiscriminatorNode matches XmlDiscriminatorValue below:
@XmlDiscriminatorValue("PROXY_FORGING")
public class ProxyForgingTransactionData extends TransactionData {
@Schema(example = "forger_public_key")

View File

@ -39,7 +39,8 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
JoinGroupTransactionData.class, LeaveGroupTransactionData.class,
GroupApprovalTransactionData.class, SetGroupTransactionData.class,
UpdateAssetTransactionData.class,
AccountFlagsTransactionData.class, EnableForgingTransactionData.class, ProxyForgingTransactionData.class
AccountFlagsTransactionData.class, EnableForgingTransactionData.class, ProxyForgingTransactionData.class,
AccountLevelTransactionData.class
})
//All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)

View File

@ -0,0 +1,75 @@
package org.qora.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.qora.data.network.OnlineAccount;
import org.qora.transform.Transformer;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
public class GetOnlineAccountsMessage extends Message {
private static final int MAX_ACCOUNT_COUNT = 1000;
private List<OnlineAccount> onlineAccounts;
public GetOnlineAccountsMessage(List<OnlineAccount> onlineAccounts) {
this(-1, onlineAccounts);
}
private GetOnlineAccountsMessage(int id, List<OnlineAccount> onlineAccounts) {
super(id, MessageType.GET_ONLINE_ACCOUNTS);
this.onlineAccounts = onlineAccounts;
}
public List<OnlineAccount> getOnlineAccounts() {
return this.onlineAccounts;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
final int accountCount = bytes.getInt();
if (accountCount > MAX_ACCOUNT_COUNT)
return null;
List<OnlineAccount> onlineAccounts = new ArrayList<>(accountCount);
for (int i = 0; i < accountCount; ++i) {
long timestamp = bytes.getLong();
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
bytes.get(publicKey);
onlineAccounts.add(new OnlineAccount(timestamp, null, publicKey));
}
return new GetOnlineAccountsMessage(id, onlineAccounts);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(this.onlineAccounts.size()));
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
OnlineAccount onlineAccount = this.onlineAccounts.get(i);
bytes.write(Longs.toByteArray(onlineAccount.getTimestamp()));
bytes.write(onlineAccount.getPublicKey());
}
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@ -68,7 +68,9 @@ public abstract class Message {
GET_UNCONFIRMED_TRANSACTIONS(21),
TRANSACTION_SIGNATURES(22),
GET_ARBITRARY_DATA(23),
ARBITRARY_DATA(24);
ARBITRARY_DATA(24),
GET_ONLINE_ACCOUNTS(25),
ONLINE_ACCOUNTS(26);
public final int value;
public final Method fromByteBuffer;

View File

@ -0,0 +1,82 @@
package org.qora.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.qora.data.network.OnlineAccount;
import org.qora.transform.Transformer;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
public class OnlineAccountsMessage extends Message {
private static final int MAX_ACCOUNT_COUNT = 1000;
private List<OnlineAccount> onlineAccounts;
public OnlineAccountsMessage(List<OnlineAccount> onlineAccounts) {
this(-1, onlineAccounts);
}
private OnlineAccountsMessage(int id, List<OnlineAccount> onlineAccounts) {
super(id, MessageType.ONLINE_ACCOUNTS);
this.onlineAccounts = onlineAccounts;
}
public List<OnlineAccount> getOnlineAccounts() {
return this.onlineAccounts;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
final int accountCount = bytes.getInt();
if (accountCount > MAX_ACCOUNT_COUNT)
return null;
List<OnlineAccount> onlineAccounts = new ArrayList<>(accountCount);
for (int i = 0; i < accountCount; ++i) {
long timestamp = bytes.getLong();
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
byte[] publicKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
bytes.get(publicKey);
OnlineAccount onlineAccount = new OnlineAccount(timestamp, signature, publicKey);
onlineAccounts.add(onlineAccount);
}
return new OnlineAccountsMessage(id, onlineAccounts);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(this.onlineAccounts.size()));
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
OnlineAccount onlineAccount = this.onlineAccounts.get(i);
bytes.write(Longs.toByteArray(onlineAccount.getTimestamp()));
bytes.write(onlineAccount.getSignature());
bytes.write(onlineAccount.getPublicKey());
}
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@ -23,6 +23,9 @@ public interface AccountRepository {
/** Returns account's flags or null if account not found. */
public Integer getFlags(String address) throws DataException;
/** Returns account's level or null if account not found. */
public Integer getLevel(String address) throws DataException;
/** Returns whether account exists. */
public boolean accountExists(String address) throws DataException;
@ -57,6 +60,13 @@ public interface AccountRepository {
*/
public void setFlags(AccountData accountData) throws DataException;
/**
* Saves account's level, and public key if present, in repository.
* <p>
* Note: ignores other fields like last reference, default groupID.
*/
public void setLevel(AccountData accountData) throws DataException;
/**
* Saves account's forging enabler, and public key if present, in repository.
* <p>
@ -97,6 +107,18 @@ public interface AccountRepository {
public List<ProxyForgerData> findProxyAccounts(List<String> recipients, List<String> forgers, List<String> involvedAddresses, Integer limit, Integer offset, Boolean reverse) throws DataException;
/**
* Returns index in list of proxy accounts (sorted by public key).
* <p>
* @return index (from 0) or null if publicKey not found in repository.
*/
public Integer getProxyAccountIndex(byte[] publicKey) throws DataException;
/**
* Returns proxy forger data using index into list of proxy accounts.
*/
public ProxyForgerData getProxyAccountByIndex(int index) throws DataException;
public void save(ProxyForgerData proxyForgerData) throws DataException;
/** Delete proxy forging relationship from repository using passed forger's public key and recipient's address. */

View File

@ -26,7 +26,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public AccountData getAccount(String address) throws DataException {
String sql = "SELECT reference, public_key, default_group_id, flags, forging_enabler FROM Accounts WHERE account = ?";
String sql = "SELECT reference, public_key, default_group_id, flags, forging_enabler, level FROM Accounts WHERE account = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
if (resultSet == null)
@ -37,8 +37,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
int defaultGroupId = resultSet.getInt(3);
int flags = resultSet.getInt(4);
String forgingEnabler = resultSet.getString(5);
int level = resultSet.getInt(6);
return new AccountData(address, reference, publicKey, defaultGroupId, flags, forgingEnabler);
return new AccountData(address, reference, publicKey, defaultGroupId, flags, forgingEnabler, level);
} catch (SQLException e) {
throw new DataException("Unable to fetch account info from repository", e);
}
@ -87,6 +88,20 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public Integer getLevel(String address) throws DataException {
String sql = "SELECT level FROM Accounts WHERE account = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
if (resultSet == null)
return null;
return resultSet.getInt(1);
} catch (SQLException e) {
throw new DataException("Unable to fetch account's level from repository", e);
}
}
@Override
public boolean accountExists(String address) throws DataException {
try {
@ -184,6 +199,23 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public void setLevel(AccountData accountData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
saveHelper.bind("account", accountData.getAddress()).bind("level", accountData.getLevel());
byte[] publicKey = accountData.getPublicKey();
if (publicKey != null)
saveHelper.bind("public_key", publicKey);
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save account's level into repository", e);
}
}
@Override
public void setForgingEnabler(AccountData accountData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
@ -520,6 +552,41 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public Integer getProxyAccountIndex(byte[] publicKey) throws DataException {
String sql = "SELECT COUNT(*) FROM ProxyForgers WHERE proxy_public_key < ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, publicKey)) {
if (resultSet == null)
return null;
return resultSet.getInt(1);
} catch (SQLException e) {
throw new DataException("Unable to determine account index in repository", e);
}
}
@Override
public ProxyForgerData getProxyAccountByIndex(int index) throws DataException {
String sql = "SELECT forger, recipient, share, proxy_public_key FROM ProxyForgers "
+ "ORDER BY proxy_public_key ASC "
+ "OFFSET ? LIMIT 1";
try (ResultSet resultSet = this.repository.checkedExecute(sql, index)) {
if (resultSet == null)
return null;
byte[] forgerPublicKey = resultSet.getBytes(1);
String recipient = resultSet.getString(2);
BigDecimal share = resultSet.getBigDecimal(3);
byte[] proxyPublicKey = resultSet.getBytes(4);
return new ProxyForgerData(forgerPublicKey, recipient, proxyPublicKey, share);
} catch (SQLException e) {
throw new DataException("Unable to fetch account info from repository", e);
}
}
@Override
public void save(ProxyForgerData proxyForgerData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("ProxyForgers");

View File

@ -22,7 +22,8 @@ import static org.qora.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli
public class HSQLDBBlockRepository implements BlockRepository {
private static final String BLOCK_DB_COLUMNS = "version, reference, transaction_count, total_fees, "
+ "transactions_signature, height, generation, generating_balance, generator, generator_signature, AT_count, AT_fees";
+ "transactions_signature, height, generation, generating_balance, generator, generator_signature, "
+ "AT_count, AT_fees, online_accounts, online_accounts_timestamp, online_accounts_signatures";
protected HSQLDBRepository repository;
@ -47,9 +48,12 @@ public class HSQLDBBlockRepository implements BlockRepository {
byte[] generatorSignature = resultSet.getBytes(10);
int atCount = resultSet.getInt(11);
BigDecimal atFees = resultSet.getBigDecimal(12);
byte[] encodedOnlineAccounts = resultSet.getBytes(13);
Long onlineAccountsTimestamp = getZonedTimestampMilli(resultSet, 14);
byte[] timestampSignatures = resultSet.getBytes(15);
return new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generatorPublicKey, generatorSignature, atCount, atFees);
generatorPublicKey, generatorSignature, atCount, atFees, encodedOnlineAccounts, onlineAccountsTimestamp, timestampSignatures);
} catch (SQLException e) {
throw new DataException("Error extracting data from result set", e);
}
@ -316,7 +320,10 @@ public class HSQLDBBlockRepository implements BlockRepository {
.bind("transactions_signature", blockData.getTransactionsSignature()).bind("height", blockData.getHeight())
.bind("generation", toOffsetDateTime(blockData.getTimestamp())).bind("generating_balance", blockData.getGeneratingBalance())
.bind("generator", blockData.getGeneratorPublicKey()).bind("generator_signature", blockData.getGeneratorSignature())
.bind("AT_count", blockData.getATCount()).bind("AT_fees", blockData.getATFees());
.bind("AT_count", blockData.getATCount()).bind("AT_fees", blockData.getATFees())
.bind("online_accounts", blockData.getEncodedOnlineAccounts())
.bind("online_accounts_timestamp", toOffsetDateTime(blockData.getOnlineAccountsTimestamp()))
.bind("online_accounts_signatures", blockData.getTimestampSignatures());
try {
saveHelper.execute(this.repository);

View File

@ -776,6 +776,21 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("ALTER TABLE Peers ADD COLUMN added_by VARCHAR(255)");
break;
case 54:
// Account 'level'
stmt.execute("ALTER TABLE Accounts ADD COLUMN level TINYINT NOT NULL DEFAULT 0");
// Corresponding transaction to set level
stmt.execute("CREATE TABLE AccountLevelTransactions (signature Signature, creator QoraPublicKey NOT NULL, target QoraAddress NOT NULL, level INT NOT NULL, "
+ "previous_level INT, PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
case 55:
// Storage of which level 1+ accounts were 'online' for a particular block. Used to distribute block rewards.
stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts VARBINARY(1048576)");
stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts_timestamp TIMESTAMP WITH TIME ZONE");
stmt.execute("ALTER TABLE Blocks ADD COLUMN online_accounts_signatures BLOB");
break;
default:
// nothing to do
return false;

View File

@ -0,0 +1,56 @@
package org.qora.repository.hsqldb.transaction;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.qora.data.transaction.AccountLevelTransactionData;
import org.qora.data.transaction.BaseTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.hsqldb.HSQLDBRepository;
import org.qora.repository.hsqldb.HSQLDBSaver;
public class HSQLDBAccountLevelTransactionRepository extends HSQLDBTransactionRepository {
public HSQLDBAccountLevelTransactionRepository(HSQLDBRepository repository) {
this.repository = repository;
}
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
String sql = "SELECT target, level, previous_level FROM AccountLevelTransactions WHERE signature = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
if (resultSet == null)
return null;
String target = resultSet.getString(1);
int level = resultSet.getInt(2);
Integer previousLevel = resultSet.getInt(3);
if (previousLevel == 0 && resultSet.wasNull())
previousLevel = null;
return new AccountLevelTransactionData(baseTransactionData, target, level, previousLevel);
} catch (SQLException e) {
throw new DataException("Unable to fetch account level transaction from repository", e);
}
}
@Override
public void save(TransactionData transactionData) throws DataException {
AccountLevelTransactionData accountLevelTransactionData = (AccountLevelTransactionData) transactionData;
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountLevelTransactions");
saveHelper.bind("signature", accountLevelTransactionData.getSignature()).bind("creator", accountLevelTransactionData.getCreatorPublicKey())
.bind("target", accountLevelTransactionData.getTarget()).bind("level", accountLevelTransactionData.getLevel())
.bind("previous_level", accountLevelTransactionData.getPreviousLevel());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save account level transaction into repository", e);
}
}
}

View File

@ -0,0 +1,118 @@
package org.qora.transaction;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import org.qora.account.Account;
import org.qora.account.GenesisAccount;
import org.qora.asset.Asset;
import org.qora.data.transaction.AccountLevelTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
public class AccountLevelTransaction extends Transaction {
// Properties
private AccountLevelTransactionData accountLevelTransactionData;
// Constructors
public AccountLevelTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData);
this.accountLevelTransactionData = (AccountLevelTransactionData) this.transactionData;
}
// More information
@Override
public List<Account> getRecipientAccounts() throws DataException {
return Collections.emptyList();
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
if (address.equals(this.getCreator().getAddress()))
return true;
if (address.equals(this.getTarget().getAddress()))
return true;
return false;
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);
if (address.equals(this.getCreator().getAddress()))
amount = amount.subtract(this.transactionData.getFee());
return amount;
}
// Navigation
public Account getTarget() {
return new Account(this.repository, this.accountLevelTransactionData.getTarget());
}
// Processing
@Override
public ValidationResult isValid() throws DataException {
Account creator = getCreator();
// Only genesis account can modify level
if (!creator.getAddress().equals(new GenesisAccount(repository).getAddress()))
return ValidationResult.NO_FLAG_PERMISSION;
// Check fee is zero or positive
if (accountLevelTransactionData.getFee().compareTo(BigDecimal.ZERO) < 0)
return ValidationResult.NEGATIVE_FEE;
// Check creator has enough funds
if (creator.getConfirmedBalance(Asset.QORA).compareTo(accountLevelTransactionData.getFee()) < 0)
return ValidationResult.NO_BALANCE;
return ValidationResult.OK;
}
@Override
public void process() throws DataException {
Account target = getTarget();
Integer previousLevel = target.getLevel();
accountLevelTransactionData.setPreviousLevel(previousLevel);
// Save this transaction with target account's previous level value
this.repository.getTransactionRepository().save(accountLevelTransactionData);
// Set account's new level
target.setLevel(this.accountLevelTransactionData.getLevel());
}
@Override
public void orphan() throws DataException {
// Revert
Account target = getTarget();
Integer previousLevel = accountLevelTransactionData.getPreviousLevel();
// If previousLevel are null then account didn't exist before this transaction
if (previousLevel == null)
this.repository.getAccountRepository().delete(target.getAddress());
else
target.setLevel(previousLevel);
// Remove previous level from transaction itself
accountLevelTransactionData.setPreviousLevel(null);
this.repository.getTransactionRepository().save(accountLevelTransactionData);
}
}

View File

@ -80,7 +80,8 @@ public abstract class Transaction {
UPDATE_ASSET(35, true),
ACCOUNT_FLAGS(36, false),
ENABLE_FORGING(37, false),
PROXY_FORGING(38, false);
PROXY_FORGING(38, false),
ACCOUNT_LEVEL(39, false);
public final int value;
public final boolean needsApproval;

View File

@ -27,6 +27,8 @@ import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import io.druid.extendedset.intset.ConciseSet;
public class BlockTransformer extends Transformer {
private static final int VERSION_LENGTH = INT_LENGTH;
@ -47,6 +49,10 @@ public class BlockTransformer extends Transformer {
protected static final int AT_FEES_LENGTH = LONG_LENGTH;
protected static final int AT_LENGTH = AT_FEES_LENGTH + AT_BYTES_LENGTH;
protected static final int ONLINE_ACCOUNTS_SIZE_LENGTH = INT_LENGTH;
protected static final int ONLINE_ACCOUNTS_TIMESTAMP_LENGTH = LONG_LENGTH;
protected static final int ONLINE_ACCOUNTS_SIGNATURES_COUNT_LENGTH = INT_LENGTH;
protected static final int V2_AT_ENTRY_LENGTH = ADDRESS_LENGTH + MD5_LENGTH;
protected static final int V4_AT_ENTRY_LENGTH = ADDRESS_LENGTH + SHA256_LENGTH + BIG_DECIMAL_LENGTH;
@ -187,13 +193,42 @@ public class BlockTransformer extends Transformer {
totalFees = totalFees.add(transactionData.getFee());
}
// Online accounts info?
byte[] onlineAccounts = null;
byte[] timestampSignatures = null;
Long onlineAccountsTimestamp = null;
if (version >= 4) {
int conciseSetLength = byteBuffer.getInt();
if (conciseSetLength > Block.MAX_BLOCK_BYTES)
throw new TransformationException("Byte data too long for online account info");
if ((conciseSetLength & 3) != 0)
throw new TransformationException("Byte data length not multiple of 4 for online account info");
onlineAccounts = new byte[conciseSetLength];
byteBuffer.get(onlineAccounts);
// Note: number of signatures, not byte length
int timestampSignaturesCount = byteBuffer.getInt();
if (timestampSignaturesCount > 0) {
// Online accounts timestamp is only present if there are also signatures
onlineAccountsTimestamp = byteBuffer.getLong();
timestampSignatures = new byte[timestampSignaturesCount * Transformer.SIGNATURE_LENGTH];
byteBuffer.get(timestampSignatures);
}
}
if (byteBuffer.hasRemaining())
throw new TransformationException("Excess byte data found after parsing Block");
// We don't have a height!
Integer height = null;
BlockData blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generatorPublicKey, generatorSignature, atCount, atFees);
generatorPublicKey, generatorSignature, atCount, atFees, onlineAccounts, onlineAccountsTimestamp, timestampSignatures);
return new Triple<BlockData, List<TransactionData>, List<ATStateData>>(blockData, transactions, atStates);
}
@ -202,9 +237,13 @@ public class BlockTransformer extends Transformer {
BlockData blockData = block.getBlockData();
int blockLength = BASE_LENGTH;
if (blockData.getVersion() >= 4)
if (blockData.getVersion() >= 4) {
blockLength += AT_BYTES_LENGTH + blockData.getATCount() * V4_AT_ENTRY_LENGTH;
else if (blockData.getVersion() >= 2)
blockLength += ONLINE_ACCOUNTS_SIZE_LENGTH + blockData.getEncodedOnlineAccounts().length;
blockLength += ONLINE_ACCOUNTS_SIGNATURES_COUNT_LENGTH;
if (blockData.getTimestampSignatures().length > 0)
blockLength += ONLINE_ACCOUNTS_TIMESTAMP_LENGTH + blockData.getTimestampSignatures().length;
} else if (blockData.getVersion() >= 2)
blockLength += AT_FEES_LENGTH + AT_BYTES_LENGTH + blockData.getATCount() * V2_AT_ENTRY_LENGTH;
try {
@ -271,6 +310,34 @@ public class BlockTransformer extends Transformer {
bytes.write(TransactionTransformer.toBytes(transactionData));
}
// Online account info
if (blockData.getVersion() >= 4) {
byte[] encodedOnlineAccounts = blockData.getEncodedOnlineAccounts();
if (encodedOnlineAccounts != null) {
bytes.write(Ints.toByteArray(encodedOnlineAccounts.length));
bytes.write(encodedOnlineAccounts);
} else {
bytes.write(Ints.toByteArray(0));
}
byte[] timestampSignatures = blockData.getTimestampSignatures();
if (timestampSignatures != null) {
// Note: we write the number of signatures, not the number of bytes
bytes.write(Ints.toByteArray(timestampSignatures.length / Transformer.SIGNATURE_LENGTH));
if (timestampSignatures.length > 0) {
// Only write online accounts timestamp if we have signatures
bytes.write(Longs.toByteArray(blockData.getOnlineAccountsTimestamp()));
bytes.write(timestampSignatures);
}
} else {
bytes.write(Ints.toByteArray(0));
}
}
return bytes.toByteArray();
} catch (IOException | DataException e) {
throw new TransformationException("Unable to serialize block", e);
@ -285,13 +352,13 @@ public class BlockTransformer extends Transformer {
byte[] generatorSignature = getGeneratorSignatureFromReference(blockData.getReference());
PublicKeyAccount generator = new PublicKeyAccount(null, blockData.getGeneratorPublicKey());
return getBytesForGeneratorSignature(generatorSignature, blockData.getGeneratingBalance(), generator);
return getBytesForGeneratorSignature(generatorSignature, blockData.getGeneratingBalance(), generator, blockData.getEncodedOnlineAccounts());
}
public static byte[] getBytesForGeneratorSignature(byte[] generatorSignature, BigDecimal generatingBalance, PublicKeyAccount generator)
public static byte[] getBytesForGeneratorSignature(byte[] generatorSignature, BigDecimal generatingBalance, PublicKeyAccount generator, byte[] encodedOnlineAccounts)
throws TransformationException {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH);
ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH + encodedOnlineAccounts.length);
bytes.write(generatorSignature);
@ -300,6 +367,8 @@ public class BlockTransformer extends Transformer {
// We're padding here just in case the generator is the genesis account whose public key is only 8 bytes long.
bytes.write(Bytes.ensureCapacity(generator.getPublicKey(), GENERATOR_LENGTH, 0));
bytes.write(encodedOnlineAccounts);
return bytes.toByteArray();
} catch (IOException e) {
throw new TransformationException(e);
@ -331,4 +400,35 @@ public class BlockTransformer extends Transformer {
}
}
public static byte[] encodeOnlineAccounts(ConciseSet onlineAccounts) {
return onlineAccounts.toByteBuffer().array();
}
public static ConciseSet decodeOnlineAccounts(byte[] encodedOnlineAccounts) {
int[] words = new int[encodedOnlineAccounts.length / 4];
ByteBuffer.wrap(encodedOnlineAccounts).asIntBuffer().get(words);
return new ConciseSet(words, false);
}
public static byte[] encodeTimestampSignatures(List<byte[]> signatures) {
byte[] encodedSignatures = new byte[signatures.size() * Transformer.SIGNATURE_LENGTH];
for (int i = 0; i < signatures.size(); ++i)
System.arraycopy(signatures.get(i), 0, encodedSignatures, i * Transformer.SIGNATURE_LENGTH, Transformer.SIGNATURE_LENGTH);
return encodedSignatures;
}
public static List<byte[]> decodeTimestampSignatures(byte[] encodedSignatures) {
List<byte[]> signatures = new ArrayList<>();
for (int i = 0; i < encodedSignatures.length; i += Transformer.SIGNATURE_LENGTH) {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
System.arraycopy(encodedSignatures, i, signature, 0, Transformer.SIGNATURE_LENGTH);
signatures.add(signature);
}
return signatures;
}
}

View File

@ -0,0 +1,93 @@
package org.qora.transform.transaction;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import org.qora.block.BlockChain;
import org.qora.data.transaction.AccountLevelTransactionData;
import org.qora.data.transaction.BaseTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.transform.TransformationException;
import org.qora.utils.Serialization;
public class AccountLevelTransactionTransformer extends TransactionTransformer {
// Property lengths
private static final int TARGET_LENGTH = ADDRESS_LENGTH;
private static final int LEVEL_LENGTH = BYTE_LENGTH;
private static final int EXTRAS_LENGTH = TARGET_LENGTH + LEVEL_LENGTH;
protected static final TransactionLayout layout;
static {
layout = new TransactionLayout();
layout.add("txType: " + TransactionType.ACCOUNT_LEVEL.valueString, TransformationType.INT);
layout.add("timestamp", TransformationType.TIMESTAMP);
layout.add("transaction's groupID", TransformationType.INT);
layout.add("reference", TransformationType.SIGNATURE);
layout.add("account's public key", TransformationType.PUBLIC_KEY);
layout.add("target account's address", TransformationType.ADDRESS);
layout.add("level", TransformationType.BYTE);
layout.add("fee", TransformationType.AMOUNT);
layout.add("signature", TransformationType.SIGNATURE);
}
public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
long timestamp = byteBuffer.getLong();
int txGroupId = 0;
if (timestamp >= BlockChain.getInstance().getQoraV2Timestamp())
txGroupId = byteBuffer.getInt();
byte[] reference = new byte[REFERENCE_LENGTH];
byteBuffer.get(reference);
byte[] creatorPublicKey = Serialization.deserializePublicKey(byteBuffer);
String target = Serialization.deserializeAddress(byteBuffer);
byte level = byteBuffer.get();
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, signature);
return new AccountLevelTransactionData(baseTransactionData, target, level);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
return getBaseLength(transactionData) + EXTRAS_LENGTH;
}
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
try {
AccountLevelTransactionData accountLevelTransactionData = (AccountLevelTransactionData) transactionData;
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
transformCommonBytes(transactionData, bytes);
Serialization.serializeAddress(bytes, accountLevelTransactionData.getTarget());
bytes.write(accountLevelTransactionData.getLevel());
Serialization.serializeBigDecimal(bytes, accountLevelTransactionData.getFee());
if (accountLevelTransactionData.getSignature() != null)
bytes.write(accountLevelTransactionData.getSignature());
return bytes.toByteArray();
} catch (IOException | ClassCastException e) {
throw new TransformationException(e);
}
}
}

View File

@ -1,173 +1,65 @@
{
"maxBalance": "10000000000",
"maxBalance": "10000000",
"blockDifficultyInterval": 10,
"minBlockTime": 60,
"maxBlockTime": 300,
"blockTimestampMargin": 2000,
"maxBytesPerUnitFee": 1024,
"unitFee": "1.0",
"useBrokenMD160ForAddresses": true,
"useBrokenMD160ForAddresses": false,
"requireGroupForApproval": false,
"defaultGroupId": 0,
"oneNamePerAccount": false,
"maxProxyRelationships": 8,
"oneNamePerAccount": true,
"maxProxyRelationships": 20,
"onlineAccountSignaturesMinLifetime": 2592000000,
"onlineAccountSignaturesMaxLifetime": 3196800000,
"genesisInfo": {
"version": 1,
"timestamp": "1400247274336",
"generatingBalance": "10000000",
"version": 4,
"timestamp": "1568360000000",
"generatingBalance": "100000",
"transactions": [
{ "type": "ISSUE_ASSET", "owner": "QLpLzqs4DW1FNJByeJ63qaqw3eAYCxfkjR", "assetName": "QORA", "description": "QORA coin", "quantity": 10000000000, "isDivisible": true, "fee": 0, "reference": "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC" },
{ "type": "GENESIS", "recipient": "QUD9y7NZqTtNwvSAUfewd7zKUGoVivVnTW", "amount": "7032468.191" },
{ "type": "GENESIS", "recipient": "QVafvKkE5bZTkq8PcXvdaxwuLNN2DGCwYk", "amount": "1716146.084" },
{ "type": "GENESIS", "recipient": "QV42QQP7frYWqsVq536g7zSk97fUpf2ZSN", "amount": "5241707.06" },
{ "type": "GENESIS", "recipient": "QgkLTm5GkepJpgr53nAgUyYRsvmyHpb2zT", "amount": "854964.0816" },
{ "type": "GENESIS", "recipient": "Qc8kN338XQULMBuUa6mTqL5tipvELDhzeZ", "amount": "769467.6734" },
{ "type": "GENESIS", "recipient": "QQ81BA75jZcpjQBLZE1qcHrXV8ARC1DEec", "amount": "85496408.16" },
{ "type": "GENESIS", "recipient": "QeoSe4DscWX4AFkNBCdm4WS1V7QkUFSQLP", "amount": "854968.3564" },
{ "type": "GENESIS", "recipient": "Qdfu3Eh21ZVHNDY1xyNaFqTTEYscSmfSsm", "amount": "85496408.16" },
{ "type": "GENESIS", "recipient": "QeDSr4abXKRg9j5hTN3TK9UGuH3umThZ42", "amount": "4445813.224" },
{ "type": "GENESIS", "recipient": "QQKDuo1txYB9E2xim79YVR6SQ1ZbJtJtFX", "amount": "47023024.49" },
{ "type": "GENESIS", "recipient": "QLeaeGr4CDA95FmeMtFh8okJRMLoq8Cge5", "amount": "170992816.3" },
{ "type": "GENESIS", "recipient": "QSwN5oa8ZHWJmc6FeAJ8Xr1SHaEuSahw1J", "amount": "3419856.326" },
{ "type": "GENESIS", "recipient": "QWnoGd4a7iXqQmNEpUtCb1x7nWgcya8QbE", "amount": "17056533.43" },
{ "type": "GENESIS", "recipient": "QbJqhsJjcy3vkzsJ1kHvgn26pQF3sZEypc", "amount": "42705455.87" },
{ "type": "GENESIS", "recipient": "QiBhBcseKzaDnHKyqEJs8z1Xx2rSb9XhBr", "amount": "141069073.5" },
{ "type": "GENESIS", "recipient": "QTwYwxBhzivFEWY5yfzyz1pqhJ8XCroKwv", "amount": "85496408.16" },
{ "type": "GENESIS", "recipient": "QfikxUU15Dy1oxbcDNEcLeU5cHvbrceq3A", "amount": "17099281.63" },
{ "type": "GENESIS", "recipient": "QhdqBmKZeQ3Hg1XUuR5nKtAkw47tuoRi2q", "amount": "12824461.22" },
{ "type": "GENESIS", "recipient": "QaVNyTqsTHA6JWMcqntcJf1u9c3qid76xH", "amount": "128244612.2" },
{ "type": "GENESIS", "recipient": "QYaDa7bmgo5L9qkcfJKjhPPrQkvGjEoc7y", "amount": "85496408.16" },
{ "type": "GENESIS", "recipient": "QQPddvWaYf4pbCyVHEVoyfC72EiaAv4JhT", "amount": "25648922.45" },
{ "type": "GENESIS", "recipient": "QSQpTNtTZMwaDuNq56Jz73KHWXaey9JrT1", "amount": "26341443.35" },
{ "type": "GENESIS", "recipient": "QVjcFWE6TnGePGJEtbNc1thwD2sgHBLvUV", "amount": "42940528.25" },
{ "type": "GENESIS", "recipient": "Qga93mWNqTuJYx6o33vjUpFH7Cn4sxLyoG", "amount": "2564892.245" },
{ "type": "GENESIS", "recipient": "QXyHKyQPJnb4ejyTkvS26x9sjWnhTTJ1Uc", "amount": "10259568.98" },
{ "type": "GENESIS", "recipient": "QLurSSgJvW7WXHDFSobgfakgqXxjoZzwUH", "amount": "85496408.16" },
{ "type": "GENESIS", "recipient": "QadxfiARcsmwzE93wqq82yXFi3ykM2qdtS", "amount": "79118376.11" },
{ "type": "GENESIS", "recipient": "QRHhhtz3Cv9RPKB1QBBfkRmRfpXx8vkRa5", "amount": "22435418.54" },
{ "type": "GENESIS", "recipient": "Qh8UnEs55n8jcnBaBwVtrTGkFFFBDyrMqH", "amount": "128757590.7" },
{ "type": "GENESIS", "recipient": "QhF7Fu3f54CTYA7zBQ223NQEssi2yAbAcx", "amount": "258481290.8" },
{ "type": "GENESIS", "recipient": "QPk9VB6tigoifrUYQrw4arBNk7i8HEgsDD", "amount": "128244612.2" },
{ "type": "GENESIS", "recipient": "QXWJnEPsdtaLQAEutJFR4ySiMUJCWDzZJX", "amount": "85496408.16" },
{ "type": "GENESIS", "recipient": "QVFs42gM4Cixf4Y5vDFvKKxRAamUPMCAVq", "amount": "85496408.16" },
{ "type": "GENESIS", "recipient": "Qec5ueWc4rcBrty47GZfFSqvLymxvcycFm", "amount": "129091026.7" },
{ "type": "GENESIS", "recipient": "QfYiztbDz1Nb9EMhgHidLycvuPN8HEcHEj", "amount": "128244612.2" },
{ "type": "GENESIS", "recipient": "QPdWsZtaZcAKqk2HWVhEVbws4qG5KUTXmg", "amount": "179285967.9" },
{ "type": "GENESIS", "recipient": "QVkNs5NcwQpsrCXpWzuMXkMehJr5mkvLVy", "amount": "8558190.456" },
{ "type": "GENESIS", "recipient": "Qg19DzyEfyZANx6JLy4GrSGF5LuZ2MLqyZ", "amount": "42748204.08" },
{ "type": "GENESIS", "recipient": "Qf3A8L5WJNHt1xZxmayrTp2d5owzdkcxM6", "amount": "50519827.58" },
{ "type": "GENESIS", "recipient": "QeKR4W6qkFJGF7Hmu7rSUzTSQiqJzZLXdt", "amount": "10216820.77" },
{ "type": "GENESIS", "recipient": "QWg7T5i3uBY3xeBLFTLYYruR15Ln11vwo4", "amount": "170992816.3" },
{ "type": "GENESIS", "recipient": "QUYdM5fHECPZxKQQAmoxoQa2aWg8TZYfPw", "amount": "85496408.16" },
{ "type": "GENESIS", "recipient": "QjhfEZCgrjUbnLRnWqWxzyYqKQpjjxkuA8", "amount": "86665653.61" },
{ "type": "GENESIS", "recipient": "QMA53u3wrzDoxC57CWUJePNdR8FoqinqUS", "amount": "85496408.16" },
{ "type": "GENESIS", "recipient": "QSuCp6mB5zNNeJKD62aq2hR9h84ks1WhHf", "amount": "161588211.4" },
{ "type": "GENESIS", "recipient": "QS2tCUk7GQefg4zGewwrumxSPmN6fgA7Xc", "amount": "170992816.3" },
{ "type": "GENESIS", "recipient": "Qcn6FZRxAgp3japtvjgUkBY6KPfbPZMZtM", "amount": "170992816.3" },
{ "type": "GENESIS", "recipient": "QZrmXZkRmjV2GwMt72Rr1ZqHJjv8raDk5J", "amount": "17099281.63" },
{ "type": "GENESIS", "recipient": "QeZzwGDfAHa132jb6r4rQHbgJstLuT8QJ3", "amount": "255875360.3" },
{ "type": "GENESIS", "recipient": "Qj3L139sMMuFvvjKQDwRnoSgKUnoMhDQs5", "amount": "76946767.34" },
{ "type": "GENESIS", "recipient": "QWJvpvbFRZHu7LRbY5MjzvrMBgzJNFYjCX", "amount": "178251461.4" },
{ "type": "GENESIS", "recipient": "QRyECqW54ywKVt4kZTEXyRY17aaFUaxzc4", "amount": "8772355.539" },
{ "type": "GENESIS", "recipient": "QgpH3K3ArkQTg15xjKqGq3BRgE3aNH9Q2P", "amount": "46766535.26" },
{ "type": "GENESIS", "recipient": "QVZ6pxi8e3K3S44zLbnrLSLwSoYT8CWbwV", "amount": "233172022.2" },
{ "type": "GENESIS", "recipient": "QNbA69dbnmwqJHLQeS9v63hSLZXXGkmtC6", "amount": "46626632.05" },
{ "type": "GENESIS", "recipient": "QgzudSKbcLUeQUhFngotVswDSkbU42dSMr", "amount": "83786479.99" },
{ "type": "GENESIS", "recipient": "QfkQ2UzKMBGPwj8Sm31SArjtXoka1ubU3i", "amount": "116345066.7" },
{ "type": "GENESIS", "recipient": "QgxHHNwawZeTmQ3i5d9enchi4T9VmzNZ5k", "amount": "155448014.8" },
{ "type": "GENESIS", "recipient": "QMNugJWNsLuV4Qmbzdf8r8RMEdXk5PNM69", "amount": "155448014.8" },
{ "type": "GENESIS", "recipient": "QVhWuJkCjStNMV4U8PtNM9Qz4PvLAEtVSj", "amount": "101041209.6" },
{ "type": "GENESIS", "recipient": "QXjNcckFG9gTr9YbiA3RrRhn3mPJ9zyR4G", "amount": "3108960.297" },
{ "type": "GENESIS", "recipient": "QThnuBadmExtxk81vhFKimSzbPaPcuPAdm", "amount": "155448014.8" },
{ "type": "GENESIS", "recipient": "QRc6sQthLHjfkmm2BUhu74g33XtkDoB7JP", "amount": "77773983.95" },
{ "type": "GENESIS", "recipient": "QcDLhirHkSbR4TLYeShLzHw61B8UGTFusk", "amount": "23317202.22" },
{ "type": "GENESIS", "recipient": "QXRnsXE6srHEf2abGh4eogs2mRsmNiuw6V", "amount": "5440680.519" },
{ "type": "GENESIS", "recipient": "QRJmEswbDw4x1kwsLyxtMS9533fv5cDvQV", "amount": "3886200.371" },
{ "type": "GENESIS", "recipient": "Qg43mCzWmFVwhVfx34g6shXnSU7U7amJNx", "amount": "6217920.593" },
{ "type": "GENESIS", "recipient": "QQ9PveFTW64yUcXEE6AxhokWCwhmn8F2TD", "amount": "8549640.816" },
{ "type": "GENESIS", "recipient": "QQaxJuTkW5XXn4DhhRekXpdXaWcsxEfCNG", "amount": "3886200.371" },
{ "type": "GENESIS", "recipient": "QifWFqW8XWL5mcNxtdr5z1LVC7XUu9tNSK", "amount": "3116732.697" },
{ "type": "GENESIS", "recipient": "QavhBKRN4vuyzHNNqcWxjcohRAJNTdTmh4", "amount": "154670774.8" },
{ "type": "GENESIS", "recipient": "QMQyR3Hybof8WpQsXPxh19AZFCj4Z4mmke", "amount": "77724007.42" },
{ "type": "GENESIS", "recipient": "QbT3GGjp1esTXtowVk2XCtBsKoRB8mkP61", "amount": "77724007.42" },
{ "type": "GENESIS", "recipient": "QT13tVMZEtbrgJEsBBcTtnyqGveC7mtqAb", "amount": "23317202.22" },
{ "type": "GENESIS", "recipient": "QegT2Ws5YjLQzEZ9YMzWsAZMBE8cAygHZN", "amount": "12606834" },
{ "type": "GENESIS", "recipient": "QXoKRBJiJGKwvdA3jkmoUhkM7y6vuMp2pn", "amount": "65117173.41" },
{ "type": "GENESIS", "recipient": "QY6SpdBzUev9ziqkmyaxESZSbdKwqGdedn", "amount": "89382608.53" },
{ "type": "GENESIS", "recipient": "QeMxyt1nEE7tbFbioc87xhiKb4szx5DsjY", "amount": "15544801.48" },
{ "type": "GENESIS", "recipient": "QcTp3THGZvJ42f2mWsQrawGvgBoSHgHZyk", "amount": "39639243.78" },
{ "type": "GENESIS", "recipient": "QjSH91mTDN6TeV1naAcfwPhmRogufV4n1u", "amount": "23317202.22" },
{ "type": "GENESIS", "recipient": "QiFLELeLm2TFWsnknzje51wMdt3Srkjz8g", "amount": "1554480.148" },
{ "type": "GENESIS", "recipient": "QhxtJ3vvhsvVU9x2j5n2R3TXzutfLMUvBR", "amount": "23317202.22" },
{ "type": "GENESIS", "recipient": "QUtUSNQfqexZZkaZ2s9LcpqjnTezPTnuAx", "amount": "15544801.48" },
{ "type": "GENESIS", "recipient": "Qg6sPLxNMYxjEDGLLaFkkWx6ip3py5fLEt", "amount": "777240.0742" },
{ "type": "GENESIS", "recipient": "QeLixskYbdkiAHmhBVMa2Pdi85YPFqw3Ed", "amount": "38862003.71" },
{ "type": "GENESIS", "recipient": "Qary17o9qvZ2fifiVC8tF5zoBJm79n18zA", "amount": "3893972.772" },
{ "type": "GENESIS", "recipient": "QLvCWDGwzwpR29XgiThMGDX2vxyFW5rFHB", "amount": "8790585.239" },
{ "type": "GENESIS", "recipient": "Qgc77fSoAoUSVJfq62GxxTin6dBtU7Y6Hb", "amount": "194310018.5" },
{ "type": "GENESIS", "recipient": "QPmPKjwPLCuRei6abuhMtMocxAEeSuLVcv", "amount": "23317202.22" },
{ "type": "GENESIS", "recipient": "QcGfZePUN7JHs9WEEkJhXGzALy4JybiS3N", "amount": "194224522.1" },
{ "type": "GENESIS", "recipient": "QSeXGwk7eQjR8j7bndJST19qWtM2qnqL1u", "amount": "38862003.71" },
{ "type": "GENESIS", "recipient": "QU9i68h71nKTg4gwc5yJHzNRQdQEswP7Kn", "amount": "139592317.3" },
{ "type": "GENESIS", "recipient": "QdKrZGCkwXSSeXJhVA1idDXsA4VFtrjPHN", "amount": "15544801.48" },
{ "type": "GENESIS", "recipient": "QiYJ2B797xFpWYFu4XWivhGhyPXLU7S5Mr", "amount": "77724007.42" },
{ "type": "GENESIS", "recipient": "QWxqtsNXUWSjYns2wdngh4WBSWQzLoQHvx", "amount": "232613963.9" },
{ "type": "GENESIS", "recipient": "QTAGfu4FpTZ1bnvnd17YPtB3zabxfWKNeM", "amount": "101041209.6" },
{ "type": "GENESIS", "recipient": "QPtRxchgRdwdnoZRwhiAoa77AvVPNSRcQk", "amount": "114254290.9" },
{ "type": "GENESIS", "recipient": "QMcfoVc9Jat2pMFLHcuPEPnY6p6uBK6Dk7", "amount": "77724007.42" },
{ "type": "GENESIS", "recipient": "Qi84KosdwSWHZX3qz4WcMgqYGutBmj14dd", "amount": "15544801.48" },
{ "type": "GENESIS", "recipient": "QjAtcHsgig2tvdGr5tR4oGmRarhuojrAK1", "amount": "2883560.675" },
{ "type": "GENESIS", "recipient": "QPJPNLP2NMHu5athB7ydezdTA6zviCV378", "amount": "6373368.608" },
{ "type": "GENESIS", "recipient": "QfVLpmLbuUnA1JEe9FmeUAzihoBvqYDp8B", "amount": "15544801.48" },
{ "type": "GENESIS", "recipient": "QVVFdy6VLFqAFCb6XSBJLLZybiKgfgDDZV", "amount": "10725913.02" },
{ "type": "GENESIS", "recipient": "QVFXyeG1xpAR8Xg3u7oAmW8unueGAfeaKi", "amount": "31221733.78" },
{ "type": "GENESIS", "recipient": "QdtQtrM1h3TLtwAGCNyoTrW5HyiPRLhrPq", "amount": "138426457.2" },
{ "type": "GENESIS", "recipient": "QMukUMr84Mi2niz6rdhEJMkKJBve7uuRfe", "amount": "116586011.1" },
{ "type": "GENESIS", "recipient": "QZR8c7dmfwqGPujebFH1miQToJZ4JQfU1X", "amount": "217938116.8" },
{ "type": "GENESIS", "recipient": "QVV5Uu8eCxufTrBtquDKA96d7Kk8S4V7yX", "amount": "40091961.25" },
{ "type": "GENESIS", "recipient": "QY9YdgfTEUFvQ2UJszGS63qkwdENkW1PQ5", "amount": "154670774.8" },
{ "type": "GENESIS", "recipient": "QNgiswyhVyNJG4UMzvoSf29wDvGZqqs7WG", "amount": "11658601.11" },
{ "type": "GENESIS", "recipient": "QabjgFiY34oihNkUcy9hpFjQdCaypCShMe", "amount": "54406805.19" },
{ "type": "GENESIS", "recipient": "QionidPRekdshCTRL3c7idWWRAqGYcKaFN", "amount": "7772400.742" },
{ "type": "GENESIS", "recipient": "QcJdBJiVgiNBNg6ZwZAiEfYDMi5ZTQaYAa", "amount": "81386689.86" },
{ "type": "GENESIS", "recipient": "QNc8XMpPwM1HESwB7kqw8HoQ5sK2miZ2un", "amount": "190423818.2" },
{ "type": "GENESIS", "recipient": "QUP1SeaNw7CvCnmDp5ai3onWYwThS4GEpu", "amount": "3886200.371" },
{ "type": "GENESIS", "recipient": "QinToqEztNN1TsLdQEuzTHh7vUrEo6JTU2", "amount": "102440241.8" },
{ "type": "GENESIS", "recipient": "QcLJYLV4RD4GmPcoNnh7dQrWeYgiiPiqFQ", "amount": "32644083.11" },
{ "type": "GENESIS", "recipient": "QdYdYGYfgmMX4jQNWMZqLr81R3HdnuiKkv", "amount": "76169527.27" },
{ "type": "GENESIS", "recipient": "Qi62mUW5zfJhgRL8FRmCpjSCCnSKtf76S6", "amount": "76169527.27" },
{ "type": "GENESIS", "recipient": "QgFkxqQGkLW6CD95N2zTnT1PPqb9nxWp6b", "amount": "76169527.27" },
{ "type": "GENESIS", "recipient": "QfNUBudYsrrq27YqiHGLUg6BtG52W1W1ci", "amount": "15544801.48" },
{ "type": "GENESIS", "recipient": "QPSFoexnGoMH7EPdg72dM7SvqA7d4M2cu7", "amount": "37307523.56" },
{ "type": "GENESIS", "recipient": "QQxt5WMvoJ2TNScAzcoxHXPnLTeQ43nQ7N", "amount": "21995894.1" },
{ "type": "GENESIS", "recipient": "QicpACxck2oDYpzP8iWRQYD4oirCtvjok9", "amount": "93268808.9" },
{ "type": "GENESIS", "recipient": "QVTJkdQkTGgqEED9kAsp4BZbYNJqWfhgGw", "amount": "153909079.5" },
{ "type": "GENESIS", "recipient": "QQL5vCkhpXnP9F4wqNiBQsNaCocmRcDSUY", "amount": "15512934.64" },
{ "type": "GENESIS", "recipient": "QSvEex3p2LaZCVBaCyL8MpYsEpHLwed17r", "amount": "155448014.8" },
{ "type": "GENESIS", "recipient": "Qb3Xv96GucQpBG8n96QVFgcs2xXsEWW4CE", "amount": "38862003.71" },
{ "type": "GENESIS", "recipient": "QdRua9MqXufALpQFDeYiQDYk3EBGdwGXSx", "amount": "230303229.1" },
{ "type": "GENESIS", "recipient": "Qh16Umei91JqiHEVWV8AC6ED9aBqbDYuph", "amount": "231073474" },
{ "type": "GENESIS", "recipient": "QMu6HXfZCnwaNmyFjjhWTYAUW7k1x7PoVr", "amount": "231073474" },
{ "type": "GENESIS", "recipient": "QgcphUTiVHHfHg8e1LVgg5jujVES7ZDUTr", "amount": "115031531" },
{ "type": "GENESIS", "recipient": "QbQk9s4j4EAxAguBhmqA8mdtTct3qGnsrx", "amount": "138348733.2" },
{ "type": "GENESIS", "recipient": "QT79PhvBwE6vFzfZ4oh5wdKVsEazZuVJFy", "amount": "6360421.343" }
{ "type": "ISSUE_ASSET", "owner": "QUwGVHPPxJNJ2dq95abQNe79EyBN2K26zM", "assetName": "QORT", "description": "QORTAL coin", "quantity": 10000000, "isDivisible": true, "fee": 0, "reference": "28u54WRcMfGujtQMZ9dNKFXVqucY7XfPihXAqPFsnx853NPUwfDJy1sMH5boCkahFgjUNYqc5fkduxdBhQTKgUsC", "data": "{}" },
{ "type": "GENESIS", "recipient": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "QcatoCyyp7dVfMtJ92sgUUPDoBJevaemRX", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "QTiga19sttbf6CLQLT83mhCSWEaCvjk8th", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "QcrowX39FuycKvMFFBsakyd5HSxe7bxFsn", "amount": "1000000" },
{ "type": "ACCOUNT_FLAGS", "target": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QcatoCyyp7dVfMtJ92sgUUPDoBJevaemRX", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QTiga19sttbf6CLQLT83mhCSWEaCvjk8th", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_FLAGS", "target": "QcrowX39FuycKvMFFBsakyd5HSxe7bxFsn", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "ACCOUNT_LEVEL", "target": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "level": 8 },
{ "type": "ACCOUNT_LEVEL", "target": "QcatoCyyp7dVfMtJ92sgUUPDoBJevaemRX", "level": 3 },
{ "type": "ACCOUNT_LEVEL", "target": "QTiga19sttbf6CLQLT83mhCSWEaCvjk8th", "level": 10 },
{ "type": "ACCOUNT_LEVEL", "target": "QcrowX39FuycKvMFFBsakyd5HSxe7bxFsn", "level": 10 },
{ "type": "CREATE_GROUP", "creatorPublicKey": "6rNn9b3pYRrG9UKqzMWYZ9qa8F3Zgv2mVWrULGHUusb", "owner": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT60", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
{ "type": "CREATE_GROUP", "creatorPublicKey": "JBNBQQDzZsm5do1BrwWAp53Ps4KYJVt749EGpCf7ofte", "owner": "QTiga19sttbf6CLQLT83mhCSWEaCvjk8th", "groupName": "Tiga", "description": "Tiga's group", "isOpen": true, "approvalThreshold": "PCT20", "minimumBlockDelay": 120, "maximumBlockDelay": 2880 },
{ "type": "PROXY_FORGING", "forgerPublicKey": "6rNn9b3pYRrG9UKqzMWYZ9qa8F3Zgv2mVWrULGHUusb", "recipient": "QcatTpaU1UneBs3fVHo8QN6mUmuceRVzFY", "proxyPublicKey": "8X3w1521UNnnonieugAxhfbfvqoRpwPXJrwGQZb5JjQ3", "share": 100 }
]
},
"rewardsByHeight": [
{ "height": 1, "reward": 0 }
{ "height": 1, "reward": 0 },
{ "height": 100, "reward": 100 },
{ "height": 200, "reward": 20 },
{ "height": 1000, "reward": 1 },
{ "height": 2000, "reward": 0 }
],
"forgingTiers": [
{ "minBlocks": 50, "maxSubAccounts": 5 },
{ "minBlocks": 50, "maxSubAccounts": 1 },
{ "minBlocks": 0, "maxSubAccounts": 0 }
],
"featureTriggers": {
"messageHeight": 99000,
"atHeight": 99000,
"messageHeight": 0,
"atHeight": 0,
"newBlockDistanceHeight": 0,
"newBlockTimingHeight": 0,
"newBlockTimestampHeight": 0,
"assetsTimestamp": 0,
"votingTimestamp": "1403715600000",
"arbitraryTimestamp": "1405702800000",
"powfixTimestamp": "1456426800000",
"v2Timestamp": "1559347200000",
"newAssetPricingTimestamp": "2000000000000",
"groupApprovalTimestamp": "2000000000000"
"votingTimestamp": 0,
"arbitraryTimestamp": 0,
"powfixTimestamp": 0,
"v2Timestamp": 0,
"newAssetPricingTimestamp": 0,
"groupApprovalTimestamp": 0
}
}

View File

@ -0,0 +1,316 @@
package org.qora.test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.Before;
import org.junit.Test;
import org.qora.account.PrivateKeyAccount;
import org.qora.account.PublicKeyAccount;
import org.qora.data.network.OnlineAccount;
import org.qora.network.message.GetOnlineAccountsMessage;
import org.qora.network.message.Message;
import org.qora.network.message.OnlineAccountsMessage;
import org.qora.repository.DataException;
import org.qora.test.common.Common;
import org.qora.test.common.FakePeer;
import org.qora.utils.ByteArray;
import com.google.common.primitives.Longs;
public class OnlineTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
private static final int MAX_PEERS = 100;
private static final int MAX_RUNNING_PEERS = 20;
private static final boolean LOG_CONNECTION_CHANGES = false;
private static final boolean LOG_ACCOUNT_CHANGES = true;
private static final boolean GET_ONLINE_UNICAST_NOT_BROADCAST = false;
private static final long ONLINE_TIMESTAMP_MODULUS = 5 * 60 * 1000;
private static List<PrivateKeyAccount> allKnownAccounts;
private static final Random random = new Random();
static class OnlinePeer extends FakePeer {
private static final long LAST_SEEN_EXPIRY_PERIOD = 6 * 60 * 1000;
private static final long ONLINE_REFRESH_INTERVAL = 4 * 60 * 1000;
private static final int MAX_CONNECTED_PEERS = 5;
private final PrivateKeyAccount account;
private List<OnlineAccount> onlineAccounts;
private long nextOnlineRefresh = 0;
public OnlinePeer(int id, PrivateKeyAccount account) {
super(id);
this.account = account;
this.onlineAccounts = Collections.synchronizedList(new ArrayList<>());
}
@Override
protected void processMessage(FakePeer peer, Message message) throws InterruptedException {
switch (message.getType()) {
case GET_ONLINE_ACCOUNTS: {
GetOnlineAccountsMessage getOnlineAccountsMessage = (GetOnlineAccountsMessage) message;
List<OnlineAccount> excludeAccounts = getOnlineAccountsMessage.getOnlineAccounts();
// Send online accounts info, excluding entries with matching timestamp & public key from excludeAccounts
List<OnlineAccount> accountsToSend;
synchronized (this.onlineAccounts) {
accountsToSend = new ArrayList<>(this.onlineAccounts);
}
Iterator<OnlineAccount> iterator = accountsToSend.iterator();
SEND_ITERATOR:
while (iterator.hasNext()) {
OnlineAccount onlineAccount = iterator.next();
for (int i = 0; i < excludeAccounts.size(); ++i) {
OnlineAccount excludeAccount = excludeAccounts.get(i);
if (onlineAccount.getTimestamp() == excludeAccount.getTimestamp() && Arrays.equals(onlineAccount.getPublicKey(), excludeAccount.getPublicKey())) {
iterator.remove();
continue SEND_ITERATOR;
}
}
}
Message onlineAccountsMessage = new OnlineAccountsMessage(accountsToSend);
this.send(peer, onlineAccountsMessage);
if (LOG_ACCOUNT_CHANGES)
System.out.println(String.format("[%d] sent %d of our %d online accounts to %d", this.getId(), accountsToSend.size(), onlineAccounts.size(), peer.getId()));
break;
}
case ONLINE_ACCOUNTS: {
OnlineAccountsMessage onlineAccountsMessage = (OnlineAccountsMessage) message;
List<OnlineAccount> onlineAccounts = onlineAccountsMessage.getOnlineAccounts();
if (LOG_ACCOUNT_CHANGES)
System.out.println(String.format("[%d] received %d online accounts from %d", this.getId(), onlineAccounts.size(), peer.getId()));
for (OnlineAccount onlineAccount : onlineAccounts)
verifyAndAddAccount(onlineAccount);
break;
}
default:
break;
}
}
private void verifyAndAddAccount(OnlineAccount onlineAccount) {
// we would check timestamp is 'recent' here
// Verify
byte[] data = Longs.toByteArray(onlineAccount.getTimestamp());
PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccount.getPublicKey());
if (!otherAccount.verify(onlineAccount.getSignature(), data)) {
System.out.println(String.format("[%d] rejecting invalid online account %s", this.getId(), otherAccount.getAddress()));
return;
}
ByteArray publicKeyBA = new ByteArray(onlineAccount.getPublicKey());
synchronized (this.onlineAccounts) {
OnlineAccount existingAccount = this.onlineAccounts.stream().filter(account -> new ByteArray(account.getPublicKey()).equals(publicKeyBA)).findFirst().orElse(null);
if (existingAccount != null) {
if (existingAccount.getTimestamp() < onlineAccount.getTimestamp()) {
this.onlineAccounts.remove(existingAccount);
if (LOG_ACCOUNT_CHANGES)
System.out.println(String.format("[%d] updated online account %s with timestamp %d (was %d)", this.getId(), otherAccount.getAddress(), onlineAccount.getTimestamp(), existingAccount.getTimestamp()));
} else {
if (LOG_ACCOUNT_CHANGES)
System.out.println(String.format("[%d] ignoring existing online account %s", this.getId(), otherAccount.getAddress()));
return;
}
} else {
if (LOG_ACCOUNT_CHANGES)
System.out.println(String.format("[%d] added online account %s with timestamp %d", this.getId(), otherAccount.getAddress(), onlineAccount.getTimestamp()));
}
this.onlineAccounts.add(onlineAccount);
}
}
@Override
protected void performIdleTasks() {
final long now = System.currentTimeMillis();
// Expire old entries
final long cutoffThreshold = now - LAST_SEEN_EXPIRY_PERIOD;
synchronized (this.onlineAccounts) {
Iterator<OnlineAccount> iterator = this.onlineAccounts.iterator();
while (iterator.hasNext()) {
OnlineAccount onlineAccount = iterator.next();
if (onlineAccount.getTimestamp() < cutoffThreshold) {
iterator.remove();
if (LOG_ACCOUNT_CHANGES) {
PublicKeyAccount otherAccount = new PublicKeyAccount(null, onlineAccount.getPublicKey());
System.out.println(String.format("[%d] removed expired online account %s with timestamp %d", this.getId(), otherAccount.getAddress(), onlineAccount.getTimestamp()));
}
}
}
}
// Request data from another peer
Message message;
synchronized (this.onlineAccounts) {
message = new GetOnlineAccountsMessage(this.onlineAccounts);
}
if (GET_ONLINE_UNICAST_NOT_BROADCAST) {
FakePeer peer = this.pickRandomPeer();
if (peer != null)
this.send(peer, message);
} else {
this.broadcast(message);
}
// Refresh our onlineness?
if (now >= this.nextOnlineRefresh) {
this.nextOnlineRefresh = now + ONLINE_REFRESH_INTERVAL;
refreshOnlineness();
}
// Log our online list
synchronized (this.onlineAccounts) {
System.out.println(String.format("[%d] Connections: %d, online accounts: %d", this.getId(), this.peers.size(), this.onlineAccounts.size()));
}
}
private void refreshOnlineness() {
// Broadcast signed timestamp
final long timestamp = (System.currentTimeMillis() / ONLINE_TIMESTAMP_MODULUS) * ONLINE_TIMESTAMP_MODULUS;
byte[] data = Longs.toByteArray(timestamp);
byte[] signature = this.account.sign(data);
byte[] publicKey = this.account.getPublicKey();
// Our account is online
OnlineAccount onlineAccount = new OnlineAccount(timestamp, signature, publicKey);
synchronized (this.onlineAccounts) {
this.onlineAccounts.removeIf(account -> account.getPublicKey() == this.account.getPublicKey());
this.onlineAccounts.add(onlineAccount);
}
Message message = new OnlineAccountsMessage(Arrays.asList(onlineAccount));
this.broadcast(message);
if (LOG_ACCOUNT_CHANGES)
System.out.println(String.format("[%d] broadcasted online account %s with timestamp %d", this.getId(), this.account.getAddress(), timestamp));
}
@Override
public void connect(FakePeer otherPeer) {
int totalPeers;
synchronized (this.peers) {
totalPeers = this.peers.size();
}
if (totalPeers >= MAX_CONNECTED_PEERS)
return;
super.connect(otherPeer);
if (LOG_CONNECTION_CHANGES)
System.out.println(String.format("[%d] Connected to peer %d, total peers: %d", this.getId(), otherPeer.getId(), totalPeers + 1));
}
public void randomDisconnect() {
FakePeer peer;
int totalPeers;
synchronized (this.peers) {
peer = this.pickRandomPeer();
if (peer == null)
return;
totalPeers = this.peers.size();
}
this.disconnect(peer);
if (LOG_CONNECTION_CHANGES)
System.out.println(String.format("[%d] Disconnected peer %d, total peers: %d", this.getId(), peer.getId(), totalPeers - 1));
}
}
@Test
public void testOnlineness() throws InterruptedException {
allKnownAccounts = new ArrayList<>();
List<OnlinePeer> allPeers = new ArrayList<>();
for (int i = 0; i < MAX_PEERS; ++i) {
byte[] seed = new byte[32];
random.nextBytes(seed);
PrivateKeyAccount account = new PrivateKeyAccount(null, seed);
allKnownAccounts.add(account);
OnlinePeer peer = new OnlinePeer(i, account);
allPeers.add(peer);
}
// Start up some peers
List<OnlinePeer> runningPeers = new ArrayList<>();
ExecutorService peerExecutor = Executors.newCachedThreadPool();
for (int c = 0; c < MAX_RUNNING_PEERS; ++c) {
OnlinePeer newPeer;
do {
int i = random.nextInt(allPeers.size());
newPeer = allPeers.get(i);
} while (runningPeers.contains(newPeer));
runningPeers.add(newPeer);
peerExecutor.execute(newPeer);
}
// Randomly connect/disconnect peers
while (true) {
int i = random.nextInt(runningPeers.size());
OnlinePeer peer = runningPeers.get(i);
if ((random.nextInt() & 0xf) != 0) {
// Connect
OnlinePeer otherPeer;
do {
int j = random.nextInt(runningPeers.size());
otherPeer = runningPeers.get(j);
} while (otherPeer == peer);
peer.connect(otherPeer);
} else {
peer.randomDisconnect();
}
Thread.sleep(100);
}
}
}

View File

@ -15,8 +15,14 @@ import org.qora.utils.Base58;
import com.google.common.hash.HashCode;
import io.druid.extendedset.intset.ConciseSet;
import static org.junit.Assert.*;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import org.junit.Before;
public class SerializationTests extends Common {
@ -70,4 +76,69 @@ public class SerializationTests extends Common {
}
}
@Test
public void testAccountBitMap() {
Random random = new Random();
final int numberOfKnownAccounts = random.nextInt(1 << 17) + 1;
System.out.println(String.format("Number of known accounts: %d", numberOfKnownAccounts));
// 5% to 15%
final int numberOfAccountsToEncode = random.nextInt((numberOfKnownAccounts / 10) + numberOfKnownAccounts / 5);
System.out.println(String.format("Number of accounts to encode: %d", numberOfAccountsToEncode));
final int bitsLength = numberOfKnownAccounts;
System.out.println(String.format("Bits to fit all accounts: %d", bitsLength));
// Enough bytes to fit at least bitsLength bits
final int byteLength = ((bitsLength - 1) >> 3) + 1;
System.out.println(String.format("Uncompressed bytes to fit all accounts: %d", byteLength));
List<Integer> accountIndexes = new LinkedList<>();
for (int i = 0; i < numberOfAccountsToEncode; ++i) {
final int accountIndex = random.nextInt(numberOfKnownAccounts);
accountIndexes.add(accountIndex);
// System.out.println(String.format("Account [%d]: %d / 0x%08x", i, accountIndex, accountIndex));
}
ConciseSet compressedSet = new ConciseSet();
for (Integer accountIndex : accountIndexes)
compressedSet.add(accountIndex);
int compressedSize = compressedSet.toByteBuffer().remaining();
System.out.println(String.format("Out of %d known accounts, encoding %d accounts needs %d uncompressed bytes but only %d compressed bytes",
numberOfKnownAccounts, numberOfAccountsToEncode, byteLength, compressedSize));
}
@Test
public void benchmarkBitSetCompression() {
Random random = new Random();
System.out.println(String.format("Known Online UncompressedBitSet UncompressedIntList Compressed"));
for (int run = 0; run < 1000; ++run) {
final int numberOfKnownAccounts = random.nextInt(1 << 17) + 1;
// 5% to 25%
final int numberOfAccountsToEncode = random.nextInt((numberOfKnownAccounts / 20) + numberOfKnownAccounts / 5);
// Enough uncompressed bytes to fit one bit per known account
final int uncompressedBitSetSize = ((numberOfKnownAccounts - 1) >> 3) + 1;
// Size of a simple list of ints
final int uncompressedIntListSize = numberOfAccountsToEncode * 4;
ConciseSet compressedSet = new ConciseSet();
for (int i = 0; i < numberOfAccountsToEncode; ++i)
compressedSet.add(random.nextInt(numberOfKnownAccounts));
int compressedSize = compressedSet.toByteBuffer().remaining();
System.out.println(String.format("%d %d %d %d %d", numberOfKnownAccounts, numberOfAccountsToEncode, uncompressedBitSetSize, uncompressedIntListSize, compressedSize));
}
}
}

View File

@ -105,7 +105,7 @@ public class TransactionTests extends Common {
// Create test generator account
generator = new PrivateKeyAccount(repository, generatorSeed);
accountRepository.setLastReference(new AccountData(generator.getAddress(), generatorSeed, generator.getPublicKey(), Group.NO_GROUP, 0, null));
accountRepository.setLastReference(new AccountData(generator.getAddress(), generatorSeed, generator.getPublicKey(), Group.NO_GROUP, 0, null, 0));
accountRepository.save(new AccountBalanceData(generator.getAddress(), Asset.QORA, initialGeneratorBalance));
// Create test sender account
@ -113,7 +113,7 @@ public class TransactionTests extends Common {
// Mock account
reference = senderSeed;
accountRepository.setLastReference(new AccountData(sender.getAddress(), reference, sender.getPublicKey(), Group.NO_GROUP, 0, null));
accountRepository.setLastReference(new AccountData(sender.getAddress(), reference, sender.getPublicKey(), Group.NO_GROUP, 0, null, 0));
// Mock balance
accountRepository.save(new AccountBalanceData(sender.getAddress(), Asset.QORA, initialSenderBalance));

View File

@ -0,0 +1,110 @@
package org.qora.test.common;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.qora.network.message.Message;
public abstract class FakePeer implements Runnable {
private static final long DEFAULT_BROADCAST_INTERVAL = 60 * 1000;
protected final int id;
protected final long startedWhen;
protected final LinkedBlockingQueue<PeerMessage> pendingMessages;
protected final List<FakePeer> peers;
private long nextBroadcast;
public FakePeer(int id) {
this.id = id;
this.startedWhen = System.currentTimeMillis();
this.pendingMessages = new LinkedBlockingQueue<>();
this.peers = Collections.synchronizedList(new ArrayList<>());
this.nextBroadcast = this.startedWhen;
}
protected static long getBroadcastInterval() {
return DEFAULT_BROADCAST_INTERVAL;
}
public int getId() {
return this.id;
}
public void run() {
try {
while (true) {
PeerMessage peerMessage = this.pendingMessages.poll(1, TimeUnit.SECONDS);
if (peerMessage != null)
processMessage(peerMessage.peer, peerMessage.message);
else
idleTasksCheck();
}
} catch (InterruptedException e) {
// fall-through to exit
}
}
protected abstract void processMessage(FakePeer peer, Message message) throws InterruptedException;
protected void idleTasksCheck() {
final long now = System.currentTimeMillis();
if (now < this.nextBroadcast)
return;
this.nextBroadcast = now + getBroadcastInterval();
this.performIdleTasks();
}
protected abstract void performIdleTasks();
public void connect(FakePeer peer) {
synchronized (this.peers) {
if (this.peers.contains(peer))
return;
this.peers.add(peer);
}
}
protected void send(FakePeer otherPeer, Message message) {
otherPeer.receive(this, message);
}
protected void broadcast(Message message) {
synchronized (this.peers) {
for (int i = 0; i < this.peers.size(); ++i)
this.send(this.peers.get(i), message);
}
}
public void receive(FakePeer sendingPeer, Message message) {
this.pendingMessages.add(new PeerMessage(sendingPeer, message));
}
public void disconnect(FakePeer peer) {
this.peers.remove(peer);
}
public FakePeer pickRandomPeer() {
synchronized (this.peers) {
if (this.peers.isEmpty())
return null;
Random random = new Random();
int i = random.nextInt(this.peers.size());
return this.peers.get(i);
}
}
}

View File

@ -0,0 +1,17 @@
package org.qora.test.common;
import org.qora.network.message.Message;
import org.qora.test.common.FakePeer;
public class PeerMessage {
public final FakePeer peer;
public final Message message;
public final long sentWhen;
public Long processedWhen = null;
public PeerMessage(FakePeer peer, Message message) {
this.peer = peer;
this.message = message;
this.sentWhen = System.currentTimeMillis();
}
}