forked from Qortal/qortal
Merge branch 'master' into qdn-file-list
This commit is contained in:
commit
328ba48224
@ -17,10 +17,10 @@
|
|||||||
<ROW Property="Manufacturer" Value="Qortal"/>
|
<ROW Property="Manufacturer" Value="Qortal"/>
|
||||||
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
||||||
<ROW Property="NTP_GOOD" Value="false"/>
|
<ROW Property="NTP_GOOD" Value="false"/>
|
||||||
<ROW Property="ProductCode" Value="1033:{ADE0C9E9-F7D9-4829-8626-8571C735C4D7} 1049:{F5230C0A-9D8C-4C70-AC72-17CECC8273B8} 2052:{D5A0760C-E5B3-4C4C-97B0-81CC445F07B9} 2057:{EF5EF0BE-0B00-4F5C-A2A0-DF2CB82FF20D} " Type="16"/>
|
<ROW Property="ProductCode" Value="1033:{6C93A96C-E3AF-42FD-BE11-7EC3734905C6} 1049:{754F5347-82E5-4251-AED0-F4141CDD11F5} 2052:{413BD7B3-A3F8-47D0-BCA4-5C7694A40936} 2057:{71450AC8-1E6F-4469-852D-0591FA693680} " Type="16"/>
|
||||||
<ROW Property="ProductLanguage" Value="2057"/>
|
<ROW Property="ProductLanguage" Value="2057"/>
|
||||||
<ROW Property="ProductName" Value="Qortal"/>
|
<ROW Property="ProductName" Value="Qortal"/>
|
||||||
<ROW Property="ProductVersion" Value="3.6.3" Type="32"/>
|
<ROW Property="ProductVersion" Value="3.8.3" Type="32"/>
|
||||||
<ROW Property="RECONFIG_NTP" Value="true"/>
|
<ROW Property="RECONFIG_NTP" Value="true"/>
|
||||||
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||||
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||||
@ -212,7 +212,7 @@
|
|||||||
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
|
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
|
||||||
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
|
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
|
||||||
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
|
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
|
||||||
<ROW Component="AI_CustomARPName" ComponentId="{F4F774B9-18DC-4740-9552-EA16B98801C9}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
<ROW Component="AI_CustomARPName" ComponentId="{EC7B4AD9-F2D9-48C4-A586-C4697D9C380C}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||||
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
||||||
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
||||||
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>
|
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>
|
||||||
|
BIN
lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar
Normal file
BIN
lib/org/ciyam/AT/1.4.0/AT-1.4.0.jar
Normal file
Binary file not shown.
9
lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
Normal file
9
lib/org/ciyam/AT/1.4.0/AT-1.4.0.pom
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>org.ciyam</groupId>
|
||||||
|
<artifactId>AT</artifactId>
|
||||||
|
<version>1.4.0</version>
|
||||||
|
<description>POM was created from install:install-file</description>
|
||||||
|
</project>
|
@ -3,14 +3,15 @@
|
|||||||
<groupId>org.ciyam</groupId>
|
<groupId>org.ciyam</groupId>
|
||||||
<artifactId>AT</artifactId>
|
<artifactId>AT</artifactId>
|
||||||
<versioning>
|
<versioning>
|
||||||
<release>1.3.8</release>
|
<release>1.4.0</release>
|
||||||
<versions>
|
<versions>
|
||||||
<version>1.3.4</version>
|
<version>1.3.4</version>
|
||||||
<version>1.3.5</version>
|
<version>1.3.5</version>
|
||||||
<version>1.3.6</version>
|
<version>1.3.6</version>
|
||||||
<version>1.3.7</version>
|
<version>1.3.7</version>
|
||||||
<version>1.3.8</version>
|
<version>1.3.8</version>
|
||||||
|
<version>1.4.0</version>
|
||||||
</versions>
|
</versions>
|
||||||
<lastUpdated>20200925114415</lastUpdated>
|
<lastUpdated>20221105114346</lastUpdated>
|
||||||
</versioning>
|
</versioning>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
4
pom.xml
4
pom.xml
@ -3,7 +3,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>org.qortal</groupId>
|
<groupId>org.qortal</groupId>
|
||||||
<artifactId>qortal</artifactId>
|
<artifactId>qortal</artifactId>
|
||||||
<version>3.6.4</version>
|
<version>3.8.4</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<properties>
|
<properties>
|
||||||
<skipTests>true</skipTests>
|
<skipTests>true</skipTests>
|
||||||
@ -11,7 +11,7 @@
|
|||||||
<bitcoinj.version>0.15.10</bitcoinj.version>
|
<bitcoinj.version>0.15.10</bitcoinj.version>
|
||||||
<bouncycastle.version>1.69</bouncycastle.version>
|
<bouncycastle.version>1.69</bouncycastle.version>
|
||||||
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
<build.timestamp>${maven.build.timestamp}</build.timestamp>
|
||||||
<ciyam-at.version>1.3.8</ciyam-at.version>
|
<ciyam-at.version>1.4.0</ciyam-at.version>
|
||||||
<commons-net.version>3.6</commons-net.version>
|
<commons-net.version>3.6</commons-net.version>
|
||||||
<commons-text.version>1.8</commons-text.version>
|
<commons-text.version>1.8</commons-text.version>
|
||||||
<commons-io.version>2.6</commons-io.version>
|
<commons-io.version>2.6</commons-io.version>
|
||||||
|
@ -211,7 +211,8 @@ public class Account {
|
|||||||
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToMint())
|
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToMint())
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (Account.isFounder(accountData.getFlags()))
|
// Founders can always mint, unless they have a penalty
|
||||||
|
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -222,6 +223,11 @@ public class Account {
|
|||||||
return this.repository.getAccountRepository().getMintedBlockCount(this.address);
|
return this.repository.getAccountRepository().getMintedBlockCount(this.address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns account's blockMintedPenalty or null if account not found in repository. */
|
||||||
|
public Integer getBlocksMintedPenalty() throws DataException {
|
||||||
|
return this.repository.getAccountRepository().getBlocksMintedPenaltyCount(this.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Returns whether account can build reward-shares.
|
/** Returns whether account can build reward-shares.
|
||||||
* <p>
|
* <p>
|
||||||
@ -243,7 +249,7 @@ public class Account {
|
|||||||
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToRewardShare())
|
if (level != null && level >= BlockChain.getInstance().getMinAccountLevelToRewardShare())
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (Account.isFounder(accountData.getFlags()))
|
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -271,7 +277,7 @@ public class Account {
|
|||||||
/**
|
/**
|
||||||
* Returns 'effective' minting level, or zero if account does not exist/cannot mint.
|
* Returns 'effective' minting level, or zero if account does not exist/cannot mint.
|
||||||
* <p>
|
* <p>
|
||||||
* For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
|
* For founder accounts with no penalty, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||||
*
|
*
|
||||||
* @return 0+
|
* @return 0+
|
||||||
* @throws DataException
|
* @throws DataException
|
||||||
@ -281,7 +287,8 @@ public class Account {
|
|||||||
if (accountData == null)
|
if (accountData == null)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
if (Account.isFounder(accountData.getFlags()))
|
// Founders are assigned a different effective minting level, as long as they have no penalty
|
||||||
|
if (Account.isFounder(accountData.getFlags()) && accountData.getBlocksMintedPenalty() == 0)
|
||||||
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
|
return BlockChain.getInstance().getFounderEffectiveMintingLevel();
|
||||||
|
|
||||||
return accountData.getLevel();
|
return accountData.getLevel();
|
||||||
@ -289,8 +296,6 @@ public class Account {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns 'effective' minting level, or zero if reward-share does not exist.
|
* Returns 'effective' minting level, or zero if reward-share does not exist.
|
||||||
* <p>
|
|
||||||
* this is being used on src/main/java/org/qortal/api/resource/AddressesResource.java to fulfil the online accounts api call
|
|
||||||
*
|
*
|
||||||
* @param repository
|
* @param repository
|
||||||
* @param rewardSharePublicKey
|
* @param rewardSharePublicKey
|
||||||
@ -309,7 +314,7 @@ public class Account {
|
|||||||
/**
|
/**
|
||||||
* Returns 'effective' minting level, with a fix for the zero level.
|
* Returns 'effective' minting level, with a fix for the zero level.
|
||||||
* <p>
|
* <p>
|
||||||
* For founder accounts, this returns "founderEffectiveMintingLevel" from blockchain config.
|
* For founder accounts with no penalty, this returns "founderEffectiveMintingLevel" from blockchain config.
|
||||||
*
|
*
|
||||||
* @param repository
|
* @param repository
|
||||||
* @param rewardSharePublicKey
|
* @param rewardSharePublicKey
|
||||||
@ -322,7 +327,7 @@ public class Account {
|
|||||||
if (rewardShareData == null)
|
if (rewardShareData == null)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
else if(!rewardShareData.getMinter().equals(rewardShareData.getRecipient()))//the minter is different than the recipient this means sponsorship
|
else if (!rewardShareData.getMinter().equals(rewardShareData.getRecipient())) // Sponsorship reward share
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
|
Account rewardShareMinter = new Account(repository, rewardShareData.getMinter());
|
||||||
|
367
src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java
Normal file
367
src/main/java/org/qortal/account/SelfSponsorshipAlgoV1.java
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
package org.qortal.account;
|
||||||
|
|
||||||
|
import org.qortal.api.resource.TransactionsResource;
|
||||||
|
import org.qortal.asset.Asset;
|
||||||
|
import org.qortal.data.account.AccountData;
|
||||||
|
import org.qortal.data.naming.NameData;
|
||||||
|
import org.qortal.data.transaction.*;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.transaction.Transaction.TransactionType;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class SelfSponsorshipAlgoV1 {
|
||||||
|
|
||||||
|
private final Repository repository;
|
||||||
|
private final String address;
|
||||||
|
private final AccountData accountData;
|
||||||
|
private final long snapshotTimestamp;
|
||||||
|
private final boolean override;
|
||||||
|
|
||||||
|
private int registeredNameCount = 0;
|
||||||
|
private int suspiciousCount = 0;
|
||||||
|
private int suspiciousPercent = 0;
|
||||||
|
private int consolidationCount = 0;
|
||||||
|
private int bulkIssuanceCount = 0;
|
||||||
|
private int recentSponsorshipCount = 0;
|
||||||
|
|
||||||
|
private List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||||
|
private final Map<String, List<TransactionData>> paymentsByAddress = new HashMap<>();
|
||||||
|
private final Set<String> sponsees = new LinkedHashSet<>();
|
||||||
|
private Set<String> consolidatedAddresses = new LinkedHashSet<>();
|
||||||
|
private final Set<String> zeroTransactionAddreses = new LinkedHashSet<>();
|
||||||
|
private final Set<String> penaltyAddresses = new LinkedHashSet<>();
|
||||||
|
|
||||||
|
public SelfSponsorshipAlgoV1(Repository repository, String address, long snapshotTimestamp, boolean override) throws DataException {
|
||||||
|
this.repository = repository;
|
||||||
|
this.address = address;
|
||||||
|
this.accountData = this.repository.getAccountRepository().getAccount(this.address);
|
||||||
|
this.snapshotTimestamp = snapshotTimestamp;
|
||||||
|
this.override = override;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return this.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getPenaltyAddresses() {
|
||||||
|
return this.penaltyAddresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void run() throws DataException {
|
||||||
|
if (this.accountData == null) {
|
||||||
|
// Nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fetchSponsorshipRewardShares();
|
||||||
|
if (this.sponsorshipRewardShares.isEmpty()) {
|
||||||
|
// Nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.findConsolidatedRewards();
|
||||||
|
this.findBulkIssuance();
|
||||||
|
this.findRegisteredNameCount();
|
||||||
|
this.findRecentSponsorshipCount();
|
||||||
|
|
||||||
|
int score = this.calculateScore();
|
||||||
|
if (score <= 0 && !override) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String newAddress = this.getDestinationAccount(this.address);
|
||||||
|
while (newAddress != null) {
|
||||||
|
// Found destination account
|
||||||
|
this.penaltyAddresses.add(newAddress);
|
||||||
|
|
||||||
|
// Run algo for this address, but in "override" mode because it has already been flagged
|
||||||
|
SelfSponsorshipAlgoV1 algoV1 = new SelfSponsorshipAlgoV1(this.repository, newAddress, this.snapshotTimestamp, true);
|
||||||
|
algoV1.run();
|
||||||
|
this.penaltyAddresses.addAll(algoV1.getPenaltyAddresses());
|
||||||
|
|
||||||
|
newAddress = this.getDestinationAccount(newAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.penaltyAddresses.add(this.address);
|
||||||
|
|
||||||
|
if (this.override || this.recentSponsorshipCount < 20) {
|
||||||
|
this.penaltyAddresses.addAll(this.consolidatedAddresses);
|
||||||
|
this.penaltyAddresses.addAll(this.zeroTransactionAddreses);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.penaltyAddresses.addAll(this.sponsees);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDestinationAccount(String address) throws DataException {
|
||||||
|
List<TransactionData> transferPrivsTransactions = fetchTransferPrivsForAddress(address);
|
||||||
|
if (transferPrivsTransactions.isEmpty()) {
|
||||||
|
// No TRANSFER_PRIVS transactions for this address
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountData accountData = this.repository.getAccountRepository().getAccount(address);
|
||||||
|
if (accountData == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (TransactionData transactionData : transferPrivsTransactions) {
|
||||||
|
TransferPrivsTransactionData transferPrivsTransactionData = (TransferPrivsTransactionData) transactionData;
|
||||||
|
if (Arrays.equals(transferPrivsTransactionData.getSenderPublicKey(), accountData.getPublicKey())) {
|
||||||
|
return transferPrivsTransactionData.getRecipient();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void findConsolidatedRewards() throws DataException {
|
||||||
|
List<String> sponseesThatSentRewards = new ArrayList<>();
|
||||||
|
Map<String, Integer> paymentRecipients = new HashMap<>();
|
||||||
|
|
||||||
|
// Collect outgoing payments of each sponsee
|
||||||
|
for (String sponseeAddress : this.sponsees) {
|
||||||
|
|
||||||
|
// Firstly fetch all payments for address, since the functions below depend on this data
|
||||||
|
this.fetchPaymentsForAddress(sponseeAddress);
|
||||||
|
|
||||||
|
// Check if the address has zero relevant transactions
|
||||||
|
if (this.hasZeroTransactions(sponseeAddress)) {
|
||||||
|
this.zeroTransactionAddreses.add(sponseeAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get payment recipients
|
||||||
|
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
|
||||||
|
if (allPaymentRecipients.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sponseesThatSentRewards.add(sponseeAddress);
|
||||||
|
|
||||||
|
List<String> addressesPaidByThisSponsee = new ArrayList<>();
|
||||||
|
for (String paymentRecipient : allPaymentRecipients) {
|
||||||
|
if (addressesPaidByThisSponsee.contains(paymentRecipient)) {
|
||||||
|
// We already tracked this association - don't allow multiple to stack up
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
addressesPaidByThisSponsee.add(paymentRecipient);
|
||||||
|
|
||||||
|
// Increment count for this recipient, or initialize to 1 if not present
|
||||||
|
if (paymentRecipients.computeIfPresent(paymentRecipient, (k, v) -> v + 1) == null) {
|
||||||
|
paymentRecipients.put(paymentRecipient, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude addresses with a low number of payments
|
||||||
|
Map<String, Integer> filteredPaymentRecipients = paymentRecipients.entrySet().stream()
|
||||||
|
.filter(p -> p.getValue() != null && p.getValue() >= 10)
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
|
||||||
|
// Now check how many sponsees have sent to this subset of addresses
|
||||||
|
Map<String, Integer> sponseesThatConsolidatedRewards = new HashMap<>();
|
||||||
|
for (String sponseeAddress : sponseesThatSentRewards) {
|
||||||
|
List<String> allPaymentRecipients = this.fetchOutgoingPaymentRecipientsForAddress(sponseeAddress);
|
||||||
|
// Remove any that aren't to one of the flagged recipients (i.e. consolidation)
|
||||||
|
allPaymentRecipients.removeIf(r -> !filteredPaymentRecipients.containsKey(r));
|
||||||
|
|
||||||
|
int count = allPaymentRecipients.size();
|
||||||
|
if (count == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (sponseesThatConsolidatedRewards.computeIfPresent(sponseeAddress, (k, v) -> v + count) == null) {
|
||||||
|
sponseesThatConsolidatedRewards.put(sponseeAddress, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove sponsees that have only sent a low number of payments to the filtered addresses
|
||||||
|
Map<String, Integer> filteredSponseesThatConsolidatedRewards = sponseesThatConsolidatedRewards.entrySet().stream()
|
||||||
|
.filter(p -> p.getValue() != null && p.getValue() >= 2)
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
|
||||||
|
this.consolidationCount = sponseesThatConsolidatedRewards.size();
|
||||||
|
this.consolidatedAddresses = new LinkedHashSet<>(filteredSponseesThatConsolidatedRewards.keySet());
|
||||||
|
this.suspiciousCount = this.consolidationCount + this.zeroTransactionAddreses.size();
|
||||||
|
this.suspiciousPercent = (int)(this.suspiciousCount / (float) this.sponsees.size() * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void findBulkIssuance() {
|
||||||
|
Long lastTimestamp = null;
|
||||||
|
for (RewardShareTransactionData rewardShareTransactionData : sponsorshipRewardShares) {
|
||||||
|
long timestamp = rewardShareTransactionData.getTimestamp();
|
||||||
|
if (timestamp >= this.snapshotTimestamp) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastTimestamp != null) {
|
||||||
|
if (timestamp - lastTimestamp < 3*60*1000L) {
|
||||||
|
this.bulkIssuanceCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastTimestamp = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void findRegisteredNameCount() throws DataException {
|
||||||
|
int registeredNameCount = 0;
|
||||||
|
for (String sponseeAddress : sponsees) {
|
||||||
|
List<NameData> names = repository.getNameRepository().getNamesByOwner(sponseeAddress);
|
||||||
|
for (NameData name : names) {
|
||||||
|
if (name.getRegistered() < this.snapshotTimestamp) {
|
||||||
|
registeredNameCount++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.registeredNameCount = registeredNameCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void findRecentSponsorshipCount() {
|
||||||
|
final long referenceTimestamp = this.snapshotTimestamp - (365 * 24 * 60 * 60 * 1000L);
|
||||||
|
int recentSponsorshipCount = 0;
|
||||||
|
for (RewardShareTransactionData rewardShare : sponsorshipRewardShares) {
|
||||||
|
if (rewardShare.getTimestamp() >= referenceTimestamp) {
|
||||||
|
recentSponsorshipCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.recentSponsorshipCount = recentSponsorshipCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int calculateScore() {
|
||||||
|
final int suspiciousMultiplier = (this.suspiciousCount >= 100) ? this.suspiciousPercent : 1;
|
||||||
|
final int nameMultiplier = (this.sponsees.size() >= 50 && this.registeredNameCount == 0) ? 2 : 1;
|
||||||
|
final int consolidationMultiplier = Math.max(this.consolidationCount, 1);
|
||||||
|
final int bulkIssuanceMultiplier = Math.max(this.bulkIssuanceCount / 2, 1);
|
||||||
|
final int offset = 9;
|
||||||
|
return suspiciousMultiplier * nameMultiplier * consolidationMultiplier * bulkIssuanceMultiplier - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchSponsorshipRewardShares() throws DataException {
|
||||||
|
List<RewardShareTransactionData> sponsorshipRewardShares = new ArrayList<>();
|
||||||
|
|
||||||
|
// Define relevant transactions
|
||||||
|
List<TransactionType> txTypes = List.of(TransactionType.REWARD_SHARE);
|
||||||
|
List<TransactionData> transactionDataList = fetchTransactions(repository, txTypes, this.address, false);
|
||||||
|
|
||||||
|
for (TransactionData transactionData : transactionDataList) {
|
||||||
|
if (transactionData.getType() != TransactionType.REWARD_SHARE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RewardShareTransactionData rewardShareTransactionData = (RewardShareTransactionData) transactionData;
|
||||||
|
|
||||||
|
// Skip removals
|
||||||
|
if (rewardShareTransactionData.getSharePercent() < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if not sponsored by this account
|
||||||
|
if (!Arrays.equals(rewardShareTransactionData.getCreatorPublicKey(), accountData.getPublicKey())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip self shares
|
||||||
|
if (Objects.equals(rewardShareTransactionData.getRecipient(), this.address)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean duplicateFound = false;
|
||||||
|
for (RewardShareTransactionData existingRewardShare : sponsorshipRewardShares) {
|
||||||
|
if (Objects.equals(existingRewardShare.getRecipient(), rewardShareTransactionData.getRecipient())) {
|
||||||
|
// Duplicate
|
||||||
|
duplicateFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!duplicateFound) {
|
||||||
|
sponsorshipRewardShares.add(rewardShareTransactionData);
|
||||||
|
this.sponsees.add(rewardShareTransactionData.getRecipient());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sponsorshipRewardShares = sponsorshipRewardShares;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<TransactionData> fetchTransferPrivsForAddress(String address) throws DataException {
|
||||||
|
return fetchTransactions(repository,
|
||||||
|
List.of(TransactionType.TRANSFER_PRIVS),
|
||||||
|
address, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchPaymentsForAddress(String address) throws DataException {
|
||||||
|
List<TransactionData> payments = fetchTransactions(repository,
|
||||||
|
Arrays.asList(TransactionType.PAYMENT, TransactionType.TRANSFER_ASSET),
|
||||||
|
address, false);
|
||||||
|
this.paymentsByAddress.put(address, payments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> fetchOutgoingPaymentRecipientsForAddress(String address) {
|
||||||
|
List<String> outgoingPaymentRecipients = new ArrayList<>();
|
||||||
|
|
||||||
|
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
|
||||||
|
if (transactionDataList == null) transactionDataList = new ArrayList<>();
|
||||||
|
transactionDataList.removeIf(t -> t.getTimestamp() >= this.snapshotTimestamp);
|
||||||
|
for (TransactionData transactionData : transactionDataList) {
|
||||||
|
switch (transactionData.getType()) {
|
||||||
|
|
||||||
|
case PAYMENT:
|
||||||
|
PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData;
|
||||||
|
if (!Objects.equals(paymentTransactionData.getRecipient(), address)) {
|
||||||
|
// Outgoing payment from this account
|
||||||
|
outgoingPaymentRecipients.add(paymentTransactionData.getRecipient());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TRANSFER_ASSET:
|
||||||
|
TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) transactionData;
|
||||||
|
if (transferAssetTransactionData.getAssetId() == Asset.QORT) {
|
||||||
|
if (!Objects.equals(transferAssetTransactionData.getRecipient(), address)) {
|
||||||
|
// Outgoing payment from this account
|
||||||
|
outgoingPaymentRecipients.add(transferAssetTransactionData.getRecipient());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return outgoingPaymentRecipients;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasZeroTransactions(String address) {
|
||||||
|
List<TransactionData> transactionDataList = this.paymentsByAddress.get(address);
|
||||||
|
if (transactionDataList == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
transactionDataList.removeIf(t -> t.getTimestamp() >= this.snapshotTimestamp);
|
||||||
|
return transactionDataList.size() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<TransactionData> fetchTransactions(Repository repository, List<TransactionType> txTypes, String address, boolean reverse) throws DataException {
|
||||||
|
// Fetch all relevant transactions for this account
|
||||||
|
List<byte[]> signatures = repository.getTransactionRepository()
|
||||||
|
.getSignaturesMatchingCriteria(null, null, null, txTypes,
|
||||||
|
null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED,
|
||||||
|
null, null, reverse);
|
||||||
|
|
||||||
|
List<TransactionData> transactionDataList = new ArrayList<>();
|
||||||
|
|
||||||
|
for (byte[] signature : signatures) {
|
||||||
|
// Fetch transaction data
|
||||||
|
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature);
|
||||||
|
if (transactionData == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
transactionDataList.add(transactionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactionDataList;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
56
src/main/java/org/qortal/api/model/AccountPenaltyStats.java
Normal file
56
src/main/java/org/qortal/api/model/AccountPenaltyStats.java
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package org.qortal.api.model;
|
||||||
|
|
||||||
|
import org.qortal.block.SelfSponsorshipAlgoV1Block;
|
||||||
|
import org.qortal.data.account.AccountData;
|
||||||
|
import org.qortal.data.naming.NameData;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlElement;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class AccountPenaltyStats {
|
||||||
|
|
||||||
|
public Integer totalPenalties;
|
||||||
|
public Integer maxPenalty;
|
||||||
|
public Integer minPenalty;
|
||||||
|
public String penaltyHash;
|
||||||
|
|
||||||
|
protected AccountPenaltyStats() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountPenaltyStats(Integer totalPenalties, Integer maxPenalty, Integer minPenalty, String penaltyHash) {
|
||||||
|
this.totalPenalties = totalPenalties;
|
||||||
|
this.maxPenalty = maxPenalty;
|
||||||
|
this.minPenalty = minPenalty;
|
||||||
|
this.penaltyHash = penaltyHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccountPenaltyStats fromAccounts(List<AccountData> accounts) {
|
||||||
|
int totalPenalties = 0;
|
||||||
|
Integer maxPenalty = null;
|
||||||
|
Integer minPenalty = null;
|
||||||
|
|
||||||
|
List<String> addresses = new ArrayList<>();
|
||||||
|
for (AccountData accountData : accounts) {
|
||||||
|
int penalty = accountData.getBlocksMintedPenalty();
|
||||||
|
addresses.add(accountData.getAddress());
|
||||||
|
totalPenalties++;
|
||||||
|
|
||||||
|
// Penalties are expressed as a negative number, so the min and the max are reversed here
|
||||||
|
if (maxPenalty == null || penalty < maxPenalty) maxPenalty = penalty;
|
||||||
|
if (minPenalty == null || penalty > minPenalty) minPenalty = penalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
String penaltyHash = SelfSponsorshipAlgoV1Block.getHash(addresses);
|
||||||
|
return new AccountPenaltyStats(totalPenalties, maxPenalty, minPenalty, penaltyHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("totalPenalties: %d, maxPenalty: %d, minPenalty: %d, penaltyHash: %s", totalPenalties, maxPenalty, minPenalty, penaltyHash == null ? "null" : penaltyHash);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package org.qortal.api.model;
|
package org.qortal.api.model;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.data.block.BlockSummaryData;
|
import org.qortal.data.block.BlockSummaryData;
|
||||||
import org.qortal.data.network.PeerData;
|
import org.qortal.data.network.PeerData;
|
||||||
import org.qortal.network.Handshake;
|
import org.qortal.network.Handshake;
|
||||||
@ -36,6 +37,7 @@ public class ConnectedPeer {
|
|||||||
public Long lastBlockTimestamp;
|
public Long lastBlockTimestamp;
|
||||||
public UUID connectionId;
|
public UUID connectionId;
|
||||||
public String age;
|
public String age;
|
||||||
|
public Boolean isTooDivergent;
|
||||||
|
|
||||||
protected ConnectedPeer() {
|
protected ConnectedPeer() {
|
||||||
}
|
}
|
||||||
@ -69,6 +71,11 @@ public class ConnectedPeer {
|
|||||||
this.lastBlockSignature = peerChainTipData.getSignature();
|
this.lastBlockSignature = peerChainTipData.getSignature();
|
||||||
this.lastBlockTimestamp = peerChainTipData.getTimestamp();
|
this.lastBlockTimestamp = peerChainTipData.getTimestamp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only include isTooDivergent decision if we've had the opportunity to request block summaries this peer
|
||||||
|
if (peer.getLastTooDivergentTime() != null) {
|
||||||
|
this.isTooDivergent = Controller.wasRecentlyTooDivergent.test(peer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import java.math.BigDecimal;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.ws.rs.*;
|
import javax.ws.rs.*;
|
||||||
@ -27,6 +28,7 @@ import org.qortal.api.ApiErrors;
|
|||||||
import org.qortal.api.ApiException;
|
import org.qortal.api.ApiException;
|
||||||
import org.qortal.api.ApiExceptionFactory;
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
import org.qortal.api.Security;
|
import org.qortal.api.Security;
|
||||||
|
import org.qortal.api.model.AccountPenaltyStats;
|
||||||
import org.qortal.api.model.ApiOnlineAccount;
|
import org.qortal.api.model.ApiOnlineAccount;
|
||||||
import org.qortal.api.model.RewardShareKeyRequest;
|
import org.qortal.api.model.RewardShareKeyRequest;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
@ -34,6 +36,7 @@ import org.qortal.controller.LiteNode;
|
|||||||
import org.qortal.controller.OnlineAccountsManager;
|
import org.qortal.controller.OnlineAccountsManager;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.account.AccountData;
|
import org.qortal.data.account.AccountData;
|
||||||
|
import org.qortal.data.account.AccountPenaltyData;
|
||||||
import org.qortal.data.account.RewardShareData;
|
import org.qortal.data.account.RewardShareData;
|
||||||
import org.qortal.data.network.OnlineAccountData;
|
import org.qortal.data.network.OnlineAccountData;
|
||||||
import org.qortal.data.network.OnlineAccountLevel;
|
import org.qortal.data.network.OnlineAccountLevel;
|
||||||
@ -471,6 +474,54 @@ public class AddressesResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/penalties")
|
||||||
|
@Operation(
|
||||||
|
summary = "Get addresses with penalties",
|
||||||
|
description = "Returns a list of accounts with a blocksMintedPenalty",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "accounts with penalties",
|
||||||
|
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = AccountPenaltyData.class)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public List<AccountPenaltyData> getAccountsWithPenalties() {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
|
||||||
|
List<AccountPenaltyData> penalties = accounts.stream().map(a -> new AccountPenaltyData(a.getAddress(), a.getBlocksMintedPenalty())).collect(Collectors.toList());
|
||||||
|
|
||||||
|
return penalties;
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/penalties/stats")
|
||||||
|
@Operation(
|
||||||
|
summary = "Get stats about current penalties",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "aggregated stats about accounts with penalties",
|
||||||
|
content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = AccountPenaltyStats.class)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public AccountPenaltyStats getPenaltyStats() {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
|
||||||
|
return AccountPenaltyStats.fromAccounts(accounts);
|
||||||
|
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/publicize")
|
@Path("/publicize")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -1128,7 +1128,7 @@ public class ArbitraryResource {
|
|||||||
if (path == null) {
|
if (path == null) {
|
||||||
// See if we have a string instead
|
// See if we have a string instead
|
||||||
if (string != null) {
|
if (string != null) {
|
||||||
File tempFile = File.createTempFile("qortal-", ".tmp");
|
File tempFile = File.createTempFile("qortal-", "");
|
||||||
tempFile.deleteOnExit();
|
tempFile.deleteOnExit();
|
||||||
BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile.toPath().toString()));
|
BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile.toPath().toString()));
|
||||||
writer.write(string);
|
writer.write(string);
|
||||||
@ -1138,7 +1138,7 @@ public class ArbitraryResource {
|
|||||||
}
|
}
|
||||||
// ... or base64 encoded raw data
|
// ... or base64 encoded raw data
|
||||||
else if (base64 != null) {
|
else if (base64 != null) {
|
||||||
File tempFile = File.createTempFile("qortal-", ".tmp");
|
File tempFile = File.createTempFile("qortal-", "");
|
||||||
tempFile.deleteOnExit();
|
tempFile.deleteOnExit();
|
||||||
Files.write(tempFile.toPath(), Base64.decode(base64));
|
Files.write(tempFile.toPath(), Base64.decode(base64));
|
||||||
path = tempFile.toPath().toString();
|
path = tempFile.toPath().toString();
|
||||||
|
@ -634,13 +634,16 @@ public class BlocksResource {
|
|||||||
@ApiErrors({
|
@ApiErrors({
|
||||||
ApiError.REPOSITORY_ISSUE
|
ApiError.REPOSITORY_ISSUE
|
||||||
})
|
})
|
||||||
public List<BlockData> getBlockRange(@PathParam("height") int height, @Parameter(
|
public List<BlockData> getBlockRange(@PathParam("height") int height,
|
||||||
ref = "count"
|
@Parameter(ref = "count") @QueryParam("count") int count,
|
||||||
) @QueryParam("count") int count) {
|
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse,
|
||||||
|
@QueryParam("includeOnlineSignatures") Boolean includeOnlineSignatures) {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
List<BlockData> blocks = new ArrayList<>();
|
List<BlockData> blocks = new ArrayList<>();
|
||||||
|
boolean shouldReverse = (reverse != null && reverse == true);
|
||||||
|
|
||||||
for (/* count already set */; count > 0; --count, ++height) {
|
int i = 0;
|
||||||
|
while (i < count) {
|
||||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||||
if (blockData == null) {
|
if (blockData == null) {
|
||||||
// Not found - try the archive
|
// Not found - try the archive
|
||||||
@ -650,8 +653,14 @@ public class BlocksResource {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (includeOnlineSignatures == null || includeOnlineSignatures == false) {
|
||||||
|
blockData.setOnlineAccountsSignatures(null);
|
||||||
|
}
|
||||||
|
|
||||||
blocks.add(blockData);
|
blocks.add(blockData);
|
||||||
|
|
||||||
|
height = shouldReverse ? height - 1 : height + 1;
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocks;
|
return blocks;
|
||||||
|
@ -70,6 +70,8 @@ public class ChatResource {
|
|||||||
@QueryParam("txGroupId") Integer txGroupId,
|
@QueryParam("txGroupId") Integer txGroupId,
|
||||||
@QueryParam("involving") List<String> involvingAddresses,
|
@QueryParam("involving") List<String> involvingAddresses,
|
||||||
@QueryParam("reference") String reference,
|
@QueryParam("reference") String reference,
|
||||||
|
@QueryParam("chatreference") String chatReference,
|
||||||
|
@QueryParam("haschatreference") Boolean hasChatReference,
|
||||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||||
@ -92,12 +94,18 @@ public class ChatResource {
|
|||||||
if (reference != null)
|
if (reference != null)
|
||||||
referenceBytes = Base58.decode(reference);
|
referenceBytes = Base58.decode(reference);
|
||||||
|
|
||||||
|
byte[] chatReferenceBytes = null;
|
||||||
|
if (chatReference != null)
|
||||||
|
chatReferenceBytes = Base58.decode(chatReference);
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
return repository.getChatRepository().getMessagesMatchingCriteria(
|
return repository.getChatRepository().getMessagesMatchingCriteria(
|
||||||
before,
|
before,
|
||||||
after,
|
after,
|
||||||
txGroupId,
|
txGroupId,
|
||||||
referenceBytes,
|
referenceBytes,
|
||||||
|
chatReferenceBytes,
|
||||||
|
hasChatReference,
|
||||||
involvingAddresses,
|
involvingAddresses,
|
||||||
limit, offset, reverse);
|
limit, offset, reverse);
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
|
@ -68,7 +68,7 @@ public class CrossChainBitcoinResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Long balance = bitcoin.getWalletBalanceFromTransactions(key58);
|
Long balance = bitcoin.getWalletBalance(key58);
|
||||||
if (balance == null)
|
if (balance == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ public class CrossChainDigibyteResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Long balance = digibyte.getWalletBalanceFromTransactions(key58);
|
Long balance = digibyte.getWalletBalance(key58);
|
||||||
if (balance == null)
|
if (balance == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ public class CrossChainDogecoinResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Long balance = dogecoin.getWalletBalanceFromTransactions(key58);
|
Long balance = dogecoin.getWalletBalance(key58);
|
||||||
if (balance == null)
|
if (balance == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
@ -8,11 +8,10 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.ws.rs.*;
|
import javax.ws.rs.*;
|
||||||
@ -25,7 +24,6 @@ import org.bitcoinj.core.*;
|
|||||||
import org.bitcoinj.script.Script;
|
import org.bitcoinj.script.Script;
|
||||||
import org.qortal.api.*;
|
import org.qortal.api.*;
|
||||||
import org.qortal.api.model.CrossChainBitcoinyHTLCStatus;
|
import org.qortal.api.model.CrossChainBitcoinyHTLCStatus;
|
||||||
import org.qortal.controller.Controller;
|
|
||||||
import org.qortal.crosschain.*;
|
import org.qortal.crosschain.*;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.at.ATData;
|
import org.qortal.data.at.ATData;
|
||||||
@ -586,7 +584,13 @@ public class CrossChainHtlcResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<TradeBotData> allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
|
List<TradeBotData> allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
|
||||||
TradeBotData tradeBotData = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).findFirst().orElse(null);
|
List<TradeBotData> tradeBotDataList = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).collect(Collectors.toList());
|
||||||
|
if (tradeBotDataList == null || tradeBotDataList.isEmpty())
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
// Loop through all matching entries for this AT address, as there might be more than one
|
||||||
|
for (TradeBotData tradeBotData : tradeBotDataList) {
|
||||||
|
|
||||||
if (tradeBotData == null)
|
if (tradeBotData == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
@ -595,12 +599,12 @@ public class CrossChainHtlcResource {
|
|||||||
|
|
||||||
// We can't refund P2SH-A until lockTime-A has passed
|
// We can't refund P2SH-A until lockTime-A has passed
|
||||||
if (NTP.getTime() <= lockTime * 1000L)
|
if (NTP.getTime() <= lockTime * 1000L)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
continue;
|
||||||
|
|
||||||
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
|
||||||
int medianBlockTime = bitcoiny.getMedianBlockTime();
|
int medianBlockTime = bitcoiny.getMedianBlockTime();
|
||||||
if (medianBlockTime <= lockTime)
|
if (medianBlockTime <= lockTime)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
continue;
|
||||||
|
|
||||||
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
// Fee for redeem/refund is subtracted from P2SH-A balance.
|
||||||
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
|
||||||
@ -615,8 +619,7 @@ public class CrossChainHtlcResource {
|
|||||||
redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
p2shAddressA = PirateChain.getInstance().deriveP2shAddressBPrefix(redeemScriptA);
|
p2shAddressA = PirateChain.getInstance().deriveP2shAddressBPrefix(redeemScriptA);
|
||||||
htlcStatusA = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
htlcStatusA = PirateChainHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
|
||||||
p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
|
p2shAddressA = bitcoiny.deriveP2shAddress(redeemScriptA);
|
||||||
htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), p2shAddressA, minimumAmountA);
|
||||||
@ -627,16 +630,16 @@ public class CrossChainHtlcResource {
|
|||||||
case UNFUNDED:
|
case UNFUNDED:
|
||||||
case FUNDING_IN_PROGRESS:
|
case FUNDING_IN_PROGRESS:
|
||||||
// Still waiting for P2SH-A to be funded...
|
// Still waiting for P2SH-A to be funded...
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
|
continue;
|
||||||
|
|
||||||
case REDEEM_IN_PROGRESS:
|
case REDEEM_IN_PROGRESS:
|
||||||
case REDEEMED:
|
case REDEEMED:
|
||||||
case REFUND_IN_PROGRESS:
|
case REFUND_IN_PROGRESS:
|
||||||
case REFUNDED:
|
case REFUNDED:
|
||||||
// Too late!
|
// Too late!
|
||||||
return false;
|
continue;
|
||||||
|
|
||||||
case FUNDED:{
|
case FUNDED: {
|
||||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||||
|
|
||||||
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
|
if (Objects.equals(bitcoiny.getCurrencyCode(), "ARRR")) {
|
||||||
@ -659,8 +662,7 @@ public class CrossChainHtlcResource {
|
|||||||
String txid = PirateChain.getInstance().refundP2sh(p2shAddressT3,
|
String txid = PirateChain.getInstance().refundP2sh(p2shAddressT3,
|
||||||
receiveAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTime, privateKey58);
|
receiveAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTime, privateKey58);
|
||||||
LOGGER.info("Refund txid: {}", txid);
|
LOGGER.info("Refund txid: {}", txid);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// ElectrumX coins
|
// ElectrumX coins
|
||||||
|
|
||||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||||
@ -680,6 +682,7 @@ public class CrossChainHtlcResource {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
@ -68,7 +68,7 @@ public class CrossChainLitecoinResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Long balance = litecoin.getWalletBalanceFromTransactions(key58);
|
Long balance = litecoin.getWalletBalance(key58);
|
||||||
if (balance == null)
|
if (balance == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ public class CrossChainRavencoinResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Long balance = ravencoin.getWalletBalanceFromTransactions(key58);
|
Long balance = ravencoin.getWalletBalance(key58);
|
||||||
if (balance == null)
|
if (balance == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
|
||||||
|
|
||||||
|
@ -47,6 +47,8 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
|||||||
txGroupId,
|
txGroupId,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
null, null, null);
|
null, null, null);
|
||||||
|
|
||||||
sendMessages(session, chatMessages);
|
sendMessages(session, chatMessages);
|
||||||
@ -74,6 +76,8 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
involvingAddresses,
|
involvingAddresses,
|
||||||
null, null, null);
|
null, null, null);
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package org.qortal.arbitrary;
|
|||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.qortal.arbitrary.exception.DataNotPublishedException;
|
||||||
import org.qortal.arbitrary.exception.MissingDataException;
|
import org.qortal.arbitrary.exception.MissingDataException;
|
||||||
import org.qortal.arbitrary.metadata.ArbitraryDataMetadataCache;
|
import org.qortal.arbitrary.metadata.ArbitraryDataMetadataCache;
|
||||||
import org.qortal.arbitrary.misc.Service;
|
import org.qortal.arbitrary.misc.Service;
|
||||||
@ -88,7 +89,7 @@ public class ArbitraryDataBuilder {
|
|||||||
if (latestPut == null) {
|
if (latestPut == null) {
|
||||||
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
|
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
|
||||||
this.name, this.service, this.identifierString());
|
this.name, this.service, this.identifierString());
|
||||||
throw new DataException(message);
|
throw new DataNotPublishedException(message);
|
||||||
}
|
}
|
||||||
this.latestPutTransaction = latestPut;
|
this.latestPutTransaction = latestPut;
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import org.apache.commons.io.FileUtils;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
import org.qortal.arbitrary.exception.DataNotPublishedException;
|
||||||
import org.qortal.arbitrary.exception.MissingDataException;
|
import org.qortal.arbitrary.exception.MissingDataException;
|
||||||
import org.qortal.arbitrary.misc.Service;
|
import org.qortal.arbitrary.misc.Service;
|
||||||
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
||||||
@ -169,10 +170,18 @@ public class ArbitraryDataReader {
|
|||||||
this.uncompress();
|
this.uncompress();
|
||||||
this.validate();
|
this.validate();
|
||||||
|
|
||||||
|
} catch (DataNotPublishedException e) {
|
||||||
|
if (e.getMessage() != null) {
|
||||||
|
// Log the message only, to avoid spamming the logs with a full stack trace
|
||||||
|
LOGGER.debug("DataNotPublishedException when trying to load QDN resource: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
this.deleteWorkingDirectory();
|
||||||
|
throw e;
|
||||||
|
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
LOGGER.info("DataException when trying to load QDN resource", e);
|
LOGGER.info("DataException when trying to load QDN resource", e);
|
||||||
this.deleteWorkingDirectory();
|
this.deleteWorkingDirectory();
|
||||||
throw new DataException(e.getMessage());
|
throw e;
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
this.postExecute();
|
this.postExecute();
|
||||||
|
@ -3,6 +3,7 @@ package org.qortal.arbitrary;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
||||||
|
import org.qortal.arbitrary.exception.DataNotPublishedException;
|
||||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||||
import org.qortal.arbitrary.misc.Service;
|
import org.qortal.arbitrary.misc.Service;
|
||||||
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
import org.qortal.controller.arbitrary.ArbitraryDataBuildManager;
|
||||||
@ -325,7 +326,7 @@ public class ArbitraryDataResource {
|
|||||||
if (latestPut == null) {
|
if (latestPut == null) {
|
||||||
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
|
String message = String.format("Couldn't find PUT transaction for name %s, service %s and identifier %s",
|
||||||
this.resourceId, this.service, this.identifierString());
|
this.resourceId, this.service, this.identifierString());
|
||||||
throw new DataException(message);
|
throw new DataNotPublishedException(message);
|
||||||
}
|
}
|
||||||
this.latestPutTransaction = latestPut;
|
this.latestPutTransaction = latestPut;
|
||||||
|
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package org.qortal.arbitrary.exception;
|
||||||
|
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
|
||||||
|
public class DataNotPublishedException extends DataException {
|
||||||
|
|
||||||
|
public DataNotPublishedException() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataNotPublishedException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataNotPublishedException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DataNotPublishedException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -10,9 +10,7 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import static java.util.Arrays.stream;
|
import static java.util.Arrays.stream;
|
||||||
import static java.util.stream.Collectors.toMap;
|
import static java.util.stream.Collectors.toMap;
|
||||||
@ -20,9 +18,52 @@ import static java.util.stream.Collectors.toMap;
|
|||||||
public enum Service {
|
public enum Service {
|
||||||
AUTO_UPDATE(1, false, null, null),
|
AUTO_UPDATE(1, false, null, null),
|
||||||
ARBITRARY_DATA(100, false, null, null),
|
ARBITRARY_DATA(100, false, null, null),
|
||||||
|
QCHAT_ATTACHMENT(120, true, 1024*1024L, null) {
|
||||||
|
@Override
|
||||||
|
public ValidationResult validate(Path path) throws IOException {
|
||||||
|
ValidationResult superclassResult = super.validate(path);
|
||||||
|
if (superclassResult != ValidationResult.OK) {
|
||||||
|
return superclassResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom validation function to require a single file, with a whitelisted extension
|
||||||
|
int fileCount = 0;
|
||||||
|
File[] files = path.toFile().listFiles();
|
||||||
|
// If already a single file, replace the list with one that contains that file only
|
||||||
|
if (files == null && path.toFile().isFile()) {
|
||||||
|
files = new File[] { path.toFile() };
|
||||||
|
}
|
||||||
|
if (files != null) {
|
||||||
|
for (File file : files) {
|
||||||
|
if (file.getName().equals(".qortal")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
return ValidationResult.DIRECTORIES_NOT_ALLOWED;
|
||||||
|
}
|
||||||
|
final String extension = FilenameUtils.getExtension(file.getName()).toLowerCase();
|
||||||
|
// We must allow blank file extensions because these are used by data published from a plaintext or base64-encoded string
|
||||||
|
final List<String> allowedExtensions = Arrays.asList("zip", "pdf", "txt", "odt", "ods", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "");
|
||||||
|
if (extension == null || !allowedExtensions.contains(extension)) {
|
||||||
|
return ValidationResult.INVALID_FILE_EXTENSION;
|
||||||
|
}
|
||||||
|
fileCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fileCount != 1) {
|
||||||
|
return ValidationResult.INVALID_FILE_COUNT;
|
||||||
|
}
|
||||||
|
return ValidationResult.OK;
|
||||||
|
}
|
||||||
|
},
|
||||||
WEBSITE(200, true, null, null) {
|
WEBSITE(200, true, null, null) {
|
||||||
@Override
|
@Override
|
||||||
public ValidationResult validate(Path path) {
|
public ValidationResult validate(Path path) throws IOException {
|
||||||
|
ValidationResult superclassResult = super.validate(path);
|
||||||
|
if (superclassResult != ValidationResult.OK) {
|
||||||
|
return superclassResult;
|
||||||
|
}
|
||||||
|
|
||||||
// Custom validation function to require an index HTML file in the root directory
|
// Custom validation function to require an index HTML file in the root directory
|
||||||
List<String> fileNames = ArbitraryDataRenderer.indexFiles();
|
List<String> fileNames = ArbitraryDataRenderer.indexFiles();
|
||||||
String[] files = path.toFile().list();
|
String[] files = path.toFile().list();
|
||||||
@ -53,12 +94,24 @@ public enum Service {
|
|||||||
METADATA(1100, false, null, null),
|
METADATA(1100, false, null, null),
|
||||||
GIF_REPOSITORY(1200, true, 25*1024*1024L, null) {
|
GIF_REPOSITORY(1200, true, 25*1024*1024L, null) {
|
||||||
@Override
|
@Override
|
||||||
public ValidationResult validate(Path path) {
|
public ValidationResult validate(Path path) throws IOException {
|
||||||
|
ValidationResult superclassResult = super.validate(path);
|
||||||
|
if (superclassResult != ValidationResult.OK) {
|
||||||
|
return superclassResult;
|
||||||
|
}
|
||||||
|
|
||||||
// Custom validation function to require .gif files only, and at least 1
|
// Custom validation function to require .gif files only, and at least 1
|
||||||
int gifCount = 0;
|
int gifCount = 0;
|
||||||
File[] files = path.toFile().listFiles();
|
File[] files = path.toFile().listFiles();
|
||||||
|
// If already a single file, replace the list with one that contains that file only
|
||||||
|
if (files == null && path.toFile().isFile()) {
|
||||||
|
files = new File[] { path.toFile() };
|
||||||
|
}
|
||||||
if (files != null) {
|
if (files != null) {
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
|
if (file.getName().equals(".qortal")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (file.isDirectory()) {
|
if (file.isDirectory()) {
|
||||||
return ValidationResult.DIRECTORIES_NOT_ALLOWED;
|
return ValidationResult.DIRECTORIES_NOT_ALLOWED;
|
||||||
}
|
}
|
||||||
@ -143,7 +196,8 @@ public enum Service {
|
|||||||
MISSING_INDEX_FILE(4),
|
MISSING_INDEX_FILE(4),
|
||||||
DIRECTORIES_NOT_ALLOWED(5),
|
DIRECTORIES_NOT_ALLOWED(5),
|
||||||
INVALID_FILE_EXTENSION(6),
|
INVALID_FILE_EXTENSION(6),
|
||||||
MISSING_DATA(7);
|
MISSING_DATA(7),
|
||||||
|
INVALID_FILE_COUNT(8);
|
||||||
|
|
||||||
public final int value;
|
public final int value;
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ public class Block {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Lazy-instantiated expanded info on block's online accounts. */
|
/** Lazy-instantiated expanded info on block's online accounts. */
|
||||||
private static class ExpandedAccount {
|
public static class ExpandedAccount {
|
||||||
private final RewardShareData rewardShareData;
|
private final RewardShareData rewardShareData;
|
||||||
private final int sharePercent;
|
private final int sharePercent;
|
||||||
private final boolean isRecipientAlsoMinter;
|
private final boolean isRecipientAlsoMinter;
|
||||||
@ -169,6 +169,13 @@ public class Block {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Account getMintingAccount() {
|
||||||
|
return this.mintingAccount;
|
||||||
|
}
|
||||||
|
public Account getRecipientAccount() {
|
||||||
|
return this.recipientAccount;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns share bin for expanded account.
|
* Returns share bin for expanded account.
|
||||||
* <p>
|
* <p>
|
||||||
@ -363,12 +370,26 @@ public class Block {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int height = parentBlockData.getHeight() + 1;
|
||||||
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
|
long timestamp = calcTimestamp(parentBlockData, minter.getPublicKey(), minterLevel);
|
||||||
long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
|
long onlineAccountsTimestamp = OnlineAccountsManager.getCurrentOnlineAccountTimestamp();
|
||||||
|
|
||||||
// Fetch our list of online accounts, removing any that are missing a nonce
|
// Fetch our list of online accounts, removing any that are missing a nonce
|
||||||
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
|
List<OnlineAccountData> onlineAccounts = OnlineAccountsManager.getInstance().getOnlineAccounts(onlineAccountsTimestamp);
|
||||||
onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
|
onlineAccounts.removeIf(a -> a.getNonce() == null || a.getNonce() < 0);
|
||||||
|
|
||||||
|
// After feature trigger, remove any online accounts that are level 0
|
||||||
|
if (height >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||||
|
onlineAccounts.removeIf(a -> {
|
||||||
|
try {
|
||||||
|
return Account.getRewardShareEffectiveMintingLevel(repository, a.getPublicKey()) == 0;
|
||||||
|
} catch (DataException e) {
|
||||||
|
// Something went wrong, so remove the account
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (onlineAccounts.isEmpty()) {
|
if (onlineAccounts.isEmpty()) {
|
||||||
LOGGER.debug("No online accounts - not even our own?");
|
LOGGER.debug("No online accounts - not even our own?");
|
||||||
return null;
|
return null;
|
||||||
@ -435,7 +456,6 @@ public class Block {
|
|||||||
|
|
||||||
int transactionCount = 0;
|
int transactionCount = 0;
|
||||||
byte[] transactionsSignature = null;
|
byte[] transactionsSignature = null;
|
||||||
int height = parentBlockData.getHeight() + 1;
|
|
||||||
|
|
||||||
int atCount = 0;
|
int atCount = 0;
|
||||||
long atFees = 0;
|
long atFees = 0;
|
||||||
@ -1029,6 +1049,15 @@ public class Block {
|
|||||||
if (onlineRewardShares == null)
|
if (onlineRewardShares == null)
|
||||||
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
|
return ValidationResult.ONLINE_ACCOUNT_UNKNOWN;
|
||||||
|
|
||||||
|
// After feature trigger, require all online account minters to be greater than level 0
|
||||||
|
if (this.getBlockData().getHeight() >= BlockChain.getInstance().getOnlineAccountMinterLevelValidationHeight()) {
|
||||||
|
List<ExpandedAccount> expandedAccounts = this.getExpandedAccounts();
|
||||||
|
for (ExpandedAccount account : expandedAccounts) {
|
||||||
|
if (account.getMintingAccount().getEffectiveMintingLevel() == 0)
|
||||||
|
return ValidationResult.ONLINE_ACCOUNTS_INVALID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If block is past a certain age then we simply assume the signatures were correct
|
// If block is past a certain age then we simply assume the signatures were correct
|
||||||
long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime();
|
long signatureRequirementThreshold = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMinLifetime();
|
||||||
if (this.blockData.getTimestamp() < signatureRequirementThreshold)
|
if (this.blockData.getTimestamp() < signatureRequirementThreshold)
|
||||||
@ -1434,6 +1463,9 @@ public class Block {
|
|||||||
if (this.blockData.getHeight() == 212937)
|
if (this.blockData.getHeight() == 212937)
|
||||||
// Apply fix for block 212937
|
// Apply fix for block 212937
|
||||||
Block212937.processFix(this);
|
Block212937.processFix(this);
|
||||||
|
|
||||||
|
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
|
||||||
|
SelfSponsorshipAlgoV1Block.processAccountPenalties(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We're about to (test-)process a batch of transactions,
|
// We're about to (test-)process a batch of transactions,
|
||||||
@ -1490,19 +1522,23 @@ public class Block {
|
|||||||
// Batch update in repository
|
// Batch update in repository
|
||||||
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +1);
|
repository.getAccountRepository().modifyMintedBlockCounts(allUniqueExpandedAccounts.stream().map(AccountData::getAddress).collect(Collectors.toList()), +1);
|
||||||
|
|
||||||
|
// Keep track of level bumps in case we need to apply to other entries
|
||||||
|
Map<String, Integer> bumpedAccounts = new HashMap<>();
|
||||||
|
|
||||||
// Local changes and also checks for level bump
|
// Local changes and also checks for level bump
|
||||||
for (AccountData accountData : allUniqueExpandedAccounts) {
|
for (AccountData accountData : allUniqueExpandedAccounts) {
|
||||||
// Adjust count locally (in Java)
|
// Adjust count locally (in Java)
|
||||||
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
|
accountData.setBlocksMinted(accountData.getBlocksMinted() + 1);
|
||||||
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
LOGGER.trace(() -> String.format("Block minter %s up to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||||
|
|
||||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
|
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||||
|
|
||||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
||||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||||
if (newLevel > accountData.getLevel()) {
|
if (newLevel > accountData.getLevel()) {
|
||||||
// Account has increased in level!
|
// Account has increased in level!
|
||||||
accountData.setLevel(newLevel);
|
accountData.setLevel(newLevel);
|
||||||
|
bumpedAccounts.put(accountData.getAddress(), newLevel);
|
||||||
repository.getAccountRepository().setLevel(accountData);
|
repository.getAccountRepository().setLevel(accountData);
|
||||||
LOGGER.trace(() -> String.format("Block minter %s bumped to level %d", accountData.getAddress(), accountData.getLevel()));
|
LOGGER.trace(() -> String.format("Block minter %s bumped to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||||
}
|
}
|
||||||
@ -1510,6 +1546,25 @@ public class Block {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also bump other entries if need be
|
||||||
|
if (!bumpedAccounts.isEmpty()) {
|
||||||
|
for (ExpandedAccount expandedAccount : expandedAccounts) {
|
||||||
|
Integer newLevel = bumpedAccounts.get(expandedAccount.mintingAccountData.getAddress());
|
||||||
|
if (newLevel != null && expandedAccount.mintingAccountData.getLevel() != newLevel) {
|
||||||
|
expandedAccount.mintingAccountData.setLevel(newLevel);
|
||||||
|
LOGGER.trace("Also bumped {} to level {}", expandedAccount.mintingAccountData.getAddress(), newLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expandedAccount.isRecipientAlsoMinter) {
|
||||||
|
newLevel = bumpedAccounts.get(expandedAccount.recipientAccountData.getAddress());
|
||||||
|
if (newLevel != null && expandedAccount.recipientAccountData.getLevel() != newLevel) {
|
||||||
|
expandedAccount.recipientAccountData.setLevel(newLevel);
|
||||||
|
LOGGER.trace("Also bumped {} to level {}", expandedAccount.recipientAccountData.getAddress(), newLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void processBlockRewards() throws DataException {
|
protected void processBlockRewards() throws DataException {
|
||||||
@ -1669,6 +1724,9 @@ public class Block {
|
|||||||
// Revert fix for block 212937
|
// Revert fix for block 212937
|
||||||
Block212937.orphanFix(this);
|
Block212937.orphanFix(this);
|
||||||
|
|
||||||
|
else if (this.blockData.getHeight() == BlockChain.getInstance().getSelfSponsorshipAlgoV1Height())
|
||||||
|
SelfSponsorshipAlgoV1Block.orphanAccountPenalties(this);
|
||||||
|
|
||||||
// Block rewards, including transaction fees, removed after transactions undone
|
// Block rewards, including transaction fees, removed after transactions undone
|
||||||
orphanBlockRewards();
|
orphanBlockRewards();
|
||||||
|
|
||||||
@ -1797,7 +1855,7 @@ public class Block {
|
|||||||
accountData.setBlocksMinted(accountData.getBlocksMinted() - 1);
|
accountData.setBlocksMinted(accountData.getBlocksMinted() - 1);
|
||||||
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
LOGGER.trace(() -> String.format("Block minter %s down to %d minted block%s", accountData.getAddress(), accountData.getBlocksMinted(), (accountData.getBlocksMinted() != 1 ? "s" : "")));
|
||||||
|
|
||||||
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment();
|
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||||
|
|
||||||
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel)
|
||||||
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||||
|
@ -74,7 +74,11 @@ public class BlockChain {
|
|||||||
transactionV5Timestamp,
|
transactionV5Timestamp,
|
||||||
transactionV6Timestamp,
|
transactionV6Timestamp,
|
||||||
disableReferenceTimestamp,
|
disableReferenceTimestamp,
|
||||||
increaseOnlineAccountsDifficultyTimestamp;
|
increaseOnlineAccountsDifficultyTimestamp,
|
||||||
|
onlineAccountMinterLevelValidationHeight,
|
||||||
|
selfSponsorshipAlgoV1Height,
|
||||||
|
feeValidationFixTimestamp,
|
||||||
|
chatReferenceTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom transaction fees
|
// Custom transaction fees
|
||||||
@ -96,6 +100,13 @@ public class BlockChain {
|
|||||||
/** Whether only one registered name is allowed per account. */
|
/** Whether only one registered name is allowed per account. */
|
||||||
private boolean oneNamePerAccount = false;
|
private boolean oneNamePerAccount = false;
|
||||||
|
|
||||||
|
/** Checkpoints */
|
||||||
|
public static class Checkpoint {
|
||||||
|
public int height;
|
||||||
|
public String signature;
|
||||||
|
}
|
||||||
|
private List<Checkpoint> checkpoints;
|
||||||
|
|
||||||
/** Block rewards by block height */
|
/** Block rewards by block height */
|
||||||
public static class RewardByHeight {
|
public static class RewardByHeight {
|
||||||
public int height;
|
public int height;
|
||||||
@ -196,6 +207,9 @@ public class BlockChain {
|
|||||||
* featureTriggers because unit tests need to set this value via Reflection. */
|
* featureTriggers because unit tests need to set this value via Reflection. */
|
||||||
private long onlineAccountsModulusV2Timestamp;
|
private long onlineAccountsModulusV2Timestamp;
|
||||||
|
|
||||||
|
/** Snapshot timestamp for self sponsorship algo V1 */
|
||||||
|
private long selfSponsorshipAlgoV1SnapshotTimestamp;
|
||||||
|
|
||||||
/** Max reward shares by block height */
|
/** Max reward shares by block height */
|
||||||
public static class MaxRewardSharesByTimestamp {
|
public static class MaxRewardSharesByTimestamp {
|
||||||
public long timestamp;
|
public long timestamp;
|
||||||
@ -356,6 +370,11 @@ public class BlockChain {
|
|||||||
return this.onlineAccountsModulusV2Timestamp;
|
return this.onlineAccountsModulusV2Timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Self sponsorship algo
|
||||||
|
public long getSelfSponsorshipAlgoV1SnapshotTimestamp() {
|
||||||
|
return this.selfSponsorshipAlgoV1SnapshotTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
|
/** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */
|
||||||
public boolean getRequireGroupForApproval() {
|
public boolean getRequireGroupForApproval() {
|
||||||
return this.requireGroupForApproval;
|
return this.requireGroupForApproval;
|
||||||
@ -369,6 +388,10 @@ public class BlockChain {
|
|||||||
return this.oneNamePerAccount;
|
return this.oneNamePerAccount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Checkpoint> getCheckpoints() {
|
||||||
|
return this.checkpoints;
|
||||||
|
}
|
||||||
|
|
||||||
public List<RewardByHeight> getBlockRewardsByHeight() {
|
public List<RewardByHeight> getBlockRewardsByHeight() {
|
||||||
return this.rewardsByHeight;
|
return this.rewardsByHeight;
|
||||||
}
|
}
|
||||||
@ -483,6 +506,22 @@ public class BlockChain {
|
|||||||
return this.featureTriggers.get(FeatureTrigger.increaseOnlineAccountsDifficultyTimestamp.name()).longValue();
|
return this.featureTriggers.get(FeatureTrigger.increaseOnlineAccountsDifficultyTimestamp.name()).longValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getSelfSponsorshipAlgoV1Height() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.selfSponsorshipAlgoV1Height.name()).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getOnlineAccountMinterLevelValidationHeight() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.onlineAccountMinterLevelValidationHeight.name()).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFeeValidationFixTimestamp() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.feeValidationFixTimestamp.name()).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getChatReferenceTimestamp() {
|
||||||
|
return this.featureTriggers.get(FeatureTrigger.chatReferenceTimestamp.name()).longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// More complex getters for aspects that change by height or timestamp
|
// More complex getters for aspects that change by height or timestamp
|
||||||
|
|
||||||
@ -651,6 +690,7 @@ public class BlockChain {
|
|||||||
|
|
||||||
boolean isTopOnly = Settings.getInstance().isTopOnly();
|
boolean isTopOnly = Settings.getInstance().isTopOnly();
|
||||||
boolean archiveEnabled = Settings.getInstance().isArchiveEnabled();
|
boolean archiveEnabled = Settings.getInstance().isArchiveEnabled();
|
||||||
|
boolean isLite = Settings.getInstance().isLite();
|
||||||
boolean canBootstrap = Settings.getInstance().getBootstrap();
|
boolean canBootstrap = Settings.getInstance().getBootstrap();
|
||||||
boolean needsArchiveRebuild = false;
|
boolean needsArchiveRebuild = false;
|
||||||
BlockData chainTip;
|
BlockData chainTip;
|
||||||
@ -671,14 +711,37 @@ public class BlockChain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate checkpoints
|
||||||
|
// Limited to topOnly nodes for now, in order to reduce risk, and to solve a real-world problem with divergent topOnly nodes
|
||||||
|
// TODO: remove the isTopOnly conditional below once this feature has had more testing time
|
||||||
|
if (isTopOnly && !isLite) {
|
||||||
|
List<Checkpoint> checkpoints = BlockChain.getInstance().getCheckpoints();
|
||||||
|
for (Checkpoint checkpoint : checkpoints) {
|
||||||
|
BlockData blockData = repository.getBlockRepository().fromHeight(checkpoint.height);
|
||||||
|
if (blockData == null) {
|
||||||
|
// Try the archive
|
||||||
|
blockData = repository.getBlockArchiveRepository().fromHeight(checkpoint.height);
|
||||||
|
}
|
||||||
|
if (blockData == null) {
|
||||||
|
LOGGER.trace("Couldn't find block for height {}", checkpoint.height);
|
||||||
|
// This is likely due to the block being pruned, so is safe to ignore.
|
||||||
|
// Continue, as there might be other blocks we can check more definitively.
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean hasBlocks = (chainTip != null && chainTip.getHeight() > 1);
|
byte[] signature = Base58.decode(checkpoint.signature);
|
||||||
|
if (!Arrays.equals(signature, blockData.getSignature())) {
|
||||||
|
LOGGER.info("Error: block at height {} with signature {} doesn't match checkpoint sig: {}. Bootstrapping...", checkpoint.height, Base58.encode(blockData.getSignature()), checkpoint.signature);
|
||||||
|
needsArchiveRebuild = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
LOGGER.info("Block at height {} matches checkpoint signature", blockData.getHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (isTopOnly && hasBlocks) {
|
|
||||||
// Top-only mode is enabled and we have blocks, so it's possible that the genesis block has been pruned
|
|
||||||
// It's best not to validate it, and there's no real need to
|
|
||||||
} else {
|
|
||||||
// Check first block is Genesis Block
|
// Check first block is Genesis Block
|
||||||
if (!isGenesisBlockValid() || needsArchiveRebuild) {
|
if (!isGenesisBlockValid() || needsArchiveRebuild) {
|
||||||
try {
|
try {
|
||||||
@ -688,16 +751,13 @@ public class BlockChain {
|
|||||||
throw new DataException(String.format("Interrupted when trying to rebuild blockchain: %s", e.getMessage()));
|
throw new DataException(String.format("Interrupted when trying to rebuild blockchain: %s", e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// We need to create a new connection, as the previous repository and its connections may be been
|
// We need to create a new connection, as the previous repository and its connections may be been
|
||||||
// closed by rebuildBlockchain() if a bootstrap was applied
|
// closed by rebuildBlockchain() if a bootstrap was applied
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
repository.checkConsistency();
|
repository.checkConsistency();
|
||||||
|
|
||||||
// Set the number of blocks to validate based on the pruned state of the chain
|
int blocksToValidate = Math.min(Settings.getInstance().getPruneBlockLimit() - 10, 1440);
|
||||||
// If pruned, subtract an extra 10 to allow room for error
|
|
||||||
int blocksToValidate = (isTopOnly || archiveEnabled) ? Settings.getInstance().getPruneBlockLimit() - 10 : 1440;
|
|
||||||
|
|
||||||
int startHeight = Math.max(repository.getBlockRepository().getBlockchainHeight() - blocksToValidate, 1);
|
int startHeight = Math.max(repository.getBlockRepository().getBlockchainHeight() - blocksToValidate, 1);
|
||||||
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature(startHeight);
|
BlockData detachedBlockData = repository.getBlockRepository().getDetachedBlockSignature(startHeight);
|
||||||
|
133
src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java
Normal file
133
src/main/java/org/qortal/block/SelfSponsorshipAlgoV1Block.java
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package org.qortal.block;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.qortal.account.SelfSponsorshipAlgoV1;
|
||||||
|
import org.qortal.api.model.AccountPenaltyStats;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.data.account.AccountData;
|
||||||
|
import org.qortal.data.account.AccountPenaltyData;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Self Sponsorship AlgoV1 Block
|
||||||
|
* <p>
|
||||||
|
* Selected block for the initial run on the "self sponsorship detection algorithm"
|
||||||
|
*/
|
||||||
|
public final class SelfSponsorshipAlgoV1Block {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(SelfSponsorshipAlgoV1Block.class);
|
||||||
|
|
||||||
|
|
||||||
|
private SelfSponsorshipAlgoV1Block() {
|
||||||
|
/* Do not instantiate */
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void processAccountPenalties(Block block) throws DataException {
|
||||||
|
LOGGER.info("Running algo for block processing - this will take a while...");
|
||||||
|
logPenaltyStats(block.repository);
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, -5000000);
|
||||||
|
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
|
||||||
|
long totalTime = System.currentTimeMillis() - startTime;
|
||||||
|
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||||
|
LOGGER.info("{} penalty addresses processed (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
|
||||||
|
logPenaltyStats(block.repository);
|
||||||
|
|
||||||
|
int updatedCount = updateAccountLevels(block.repository, penalties);
|
||||||
|
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void orphanAccountPenalties(Block block) throws DataException {
|
||||||
|
LOGGER.info("Running algo for block orphaning - this will take a while...");
|
||||||
|
logPenaltyStats(block.repository);
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
Set<AccountPenaltyData> penalties = getAccountPenalties(block.repository, 5000000);
|
||||||
|
block.repository.getAccountRepository().updateBlocksMintedPenalties(penalties);
|
||||||
|
long totalTime = System.currentTimeMillis() - startTime;
|
||||||
|
String hash = getHash(penalties.stream().map(p -> p.getAddress()).collect(Collectors.toList()));
|
||||||
|
LOGGER.info("{} penalty addresses orphaned (hash: {}). Total time taken: {} seconds", penalties.size(), hash, (int)(totalTime / 1000.0f));
|
||||||
|
logPenaltyStats(block.repository);
|
||||||
|
|
||||||
|
int updatedCount = updateAccountLevels(block.repository, penalties);
|
||||||
|
LOGGER.info("Account levels updated for {} penalty addresses", updatedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<AccountPenaltyData> getAccountPenalties(Repository repository, int penalty) throws DataException {
|
||||||
|
final long snapshotTimestamp = BlockChain.getInstance().getSelfSponsorshipAlgoV1SnapshotTimestamp();
|
||||||
|
Set<AccountPenaltyData> penalties = new LinkedHashSet<>();
|
||||||
|
List<String> addresses = repository.getTransactionRepository().getConfirmedRewardShareCreatorsExcludingSelfShares();
|
||||||
|
for (String address : addresses) {
|
||||||
|
//System.out.println(String.format("address: %s", address));
|
||||||
|
SelfSponsorshipAlgoV1 selfSponsorshipAlgoV1 = new SelfSponsorshipAlgoV1(repository, address, snapshotTimestamp, false);
|
||||||
|
selfSponsorshipAlgoV1.run();
|
||||||
|
//System.out.println(String.format("Penalty addresses: %d", selfSponsorshipAlgoV1.getPenaltyAddresses().size()));
|
||||||
|
|
||||||
|
for (String penaltyAddress : selfSponsorshipAlgoV1.getPenaltyAddresses()) {
|
||||||
|
penalties.add(new AccountPenaltyData(penaltyAddress, penalty));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return penalties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int updateAccountLevels(Repository repository, Set<AccountPenaltyData> accountPenalties) throws DataException {
|
||||||
|
final List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
|
||||||
|
final int maximumLevel = cumulativeBlocksByLevel.size() - 1;
|
||||||
|
|
||||||
|
int updatedCount = 0;
|
||||||
|
|
||||||
|
for (AccountPenaltyData penaltyData : accountPenalties) {
|
||||||
|
AccountData accountData = repository.getAccountRepository().getAccount(penaltyData.getAddress());
|
||||||
|
final int effectiveBlocksMinted = accountData.getBlocksMinted() + accountData.getBlocksMintedAdjustment() + accountData.getBlocksMintedPenalty();
|
||||||
|
|
||||||
|
// Shortcut for penalties
|
||||||
|
if (effectiveBlocksMinted < 0) {
|
||||||
|
accountData.setLevel(0);
|
||||||
|
repository.getAccountRepository().setLevel(accountData);
|
||||||
|
updatedCount++;
|
||||||
|
LOGGER.trace(() -> String.format("Block minter %s dropped to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int newLevel = maximumLevel; newLevel >= 0; --newLevel) {
|
||||||
|
if (effectiveBlocksMinted >= cumulativeBlocksByLevel.get(newLevel)) {
|
||||||
|
accountData.setLevel(newLevel);
|
||||||
|
repository.getAccountRepository().setLevel(accountData);
|
||||||
|
updatedCount++;
|
||||||
|
LOGGER.trace(() -> String.format("Block minter %s increased to level %d", accountData.getAddress(), accountData.getLevel()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void logPenaltyStats(Repository repository) {
|
||||||
|
try {
|
||||||
|
LOGGER.info(getPenaltyStats(repository));
|
||||||
|
|
||||||
|
} catch (DataException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AccountPenaltyStats getPenaltyStats(Repository repository) throws DataException {
|
||||||
|
List<AccountData> accounts = repository.getAccountRepository().getPenaltyAccounts();
|
||||||
|
return AccountPenaltyStats.fromAccounts(accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getHash(List<String> penaltyAddresses) {
|
||||||
|
if (penaltyAddresses == null || penaltyAddresses.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Collections.sort(penaltyAddresses);
|
||||||
|
return Base58.encode(Crypto.digest(StringUtils.join(penaltyAddresses).getBytes(StandardCharsets.UTF_8)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -26,9 +26,6 @@ import org.qortal.data.block.CommonBlockData;
|
|||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.network.Network;
|
import org.qortal.network.Network;
|
||||||
import org.qortal.network.Peer;
|
import org.qortal.network.Peer;
|
||||||
import org.qortal.network.message.BlockSummariesV2Message;
|
|
||||||
import org.qortal.network.message.HeightV2Message;
|
|
||||||
import org.qortal.network.message.Message;
|
|
||||||
import org.qortal.repository.BlockRepository;
|
import org.qortal.repository.BlockRepository;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
@ -38,6 +35,8 @@ import org.qortal.transaction.Transaction;
|
|||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
|
||||||
// Minting new blocks
|
// Minting new blocks
|
||||||
|
|
||||||
public class BlockMinter extends Thread {
|
public class BlockMinter extends Thread {
|
||||||
@ -64,8 +63,8 @@ public class BlockMinter extends Thread {
|
|||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("BlockMinter");
|
Thread.currentThread().setName("BlockMinter");
|
||||||
|
|
||||||
if (Settings.getInstance().isLite()) {
|
if (Settings.getInstance().isTopOnly() || Settings.getInstance().isLite()) {
|
||||||
// Lite nodes do not mint
|
// Top only and lite nodes do not sign blocks
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
|
if (Settings.getInstance().getWipeUnconfirmedOnStart()) {
|
||||||
@ -511,6 +510,21 @@ public class BlockMinter extends Thread {
|
|||||||
|
|
||||||
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
|
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
|
||||||
|
|
||||||
|
Block block = mintTestingBlockRetainingTimestamps(repository, mintingAccount);
|
||||||
|
assertNotNull("Minted block must not be null", block);
|
||||||
|
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Block mintTestingBlockUnvalidated(Repository repository, PrivateKeyAccount... mintingAndOnlineAccounts) throws DataException {
|
||||||
|
if (!BlockChain.getInstance().isTestChain())
|
||||||
|
throw new DataException("Ignoring attempt to mint testing block for non-test chain!");
|
||||||
|
|
||||||
|
// Ensure mintingAccount is 'online' so blocks can be minted
|
||||||
|
OnlineAccountsManager.getInstance().ensureTestingAccountsOnline(mintingAndOnlineAccounts);
|
||||||
|
|
||||||
|
PrivateKeyAccount mintingAccount = mintingAndOnlineAccounts[0];
|
||||||
|
|
||||||
return mintTestingBlockRetainingTimestamps(repository, mintingAccount);
|
return mintTestingBlockRetainingTimestamps(repository, mintingAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -518,6 +532,8 @@ public class BlockMinter extends Thread {
|
|||||||
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
|
BlockData previousBlockData = repository.getBlockRepository().getLastBlock();
|
||||||
|
|
||||||
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
|
Block newBlock = Block.mint(repository, previousBlockData, mintingAccount);
|
||||||
|
if (newBlock == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
// Make sure we're the only thread modifying the blockchain
|
// Make sure we're the only thread modifying the blockchain
|
||||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||||
|
@ -29,6 +29,7 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
|
||||||
|
import org.qortal.account.Account;
|
||||||
import org.qortal.api.ApiService;
|
import org.qortal.api.ApiService;
|
||||||
import org.qortal.api.DomainMapService;
|
import org.qortal.api.DomainMapService;
|
||||||
import org.qortal.api.GatewayService;
|
import org.qortal.api.GatewayService;
|
||||||
@ -756,6 +757,28 @@ public class Controller extends Thread {
|
|||||||
return peer.isAtLeastVersion(minPeerVersion) == false;
|
return peer.isAtLeastVersion(minPeerVersion) == false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Predicate<Peer> hasInvalidSigner = peer -> {
|
||||||
|
final BlockSummaryData peerChainTipData = peer.getChainTipData();
|
||||||
|
if (peerChainTipData == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
try (Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
return Account.getRewardShareEffectiveMintingLevel(repository, peerChainTipData.getMinterPublicKey()) == 0;
|
||||||
|
} catch (DataException e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Predicate<Peer> wasRecentlyTooDivergent = peer -> {
|
||||||
|
Long now = NTP.getTime();
|
||||||
|
Long peerLastTooDivergentTime = peer.getLastTooDivergentTime();
|
||||||
|
if (now == null || peerLastTooDivergentTime == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Exclude any peers that were TOO_DIVERGENT in the last 5 mins
|
||||||
|
return (now - peerLastTooDivergentTime < 5 * 60 * 1000L);
|
||||||
|
};
|
||||||
|
|
||||||
private long getRandomRepositoryMaintenanceInterval() {
|
private long getRandomRepositoryMaintenanceInterval() {
|
||||||
final long minInterval = Settings.getInstance().getRepositoryMaintenanceMinInterval();
|
final long minInterval = Settings.getInstance().getRepositoryMaintenanceMinInterval();
|
||||||
final long maxInterval = Settings.getInstance().getRepositoryMaintenanceMaxInterval();
|
final long maxInterval = Settings.getInstance().getRepositoryMaintenanceMaxInterval();
|
||||||
|
@ -247,6 +247,9 @@ public class Synchronizer extends Thread {
|
|||||||
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
|
// Disregard peers that are on the same block as last sync attempt and we didn't like their chain
|
||||||
peers.removeIf(Controller.hasInferiorChainTip);
|
peers.removeIf(Controller.hasInferiorChainTip);
|
||||||
|
|
||||||
|
// Disregard peers that have a block with an invalid signer
|
||||||
|
peers.removeIf(Controller.hasInvalidSigner);
|
||||||
|
|
||||||
final int peersBeforeComparison = peers.size();
|
final int peersBeforeComparison = peers.size();
|
||||||
|
|
||||||
// Request recent block summaries from the remaining peers, and locate our common block with each
|
// Request recent block summaries from the remaining peers, and locate our common block with each
|
||||||
@ -1118,6 +1121,7 @@ public class Synchronizer extends Thread {
|
|||||||
// If common block is too far behind us then we're on massively different forks so give up.
|
// If common block is too far behind us then we're on massively different forks so give up.
|
||||||
if (!force && testHeight < ourHeight - MAXIMUM_COMMON_DELTA) {
|
if (!force && testHeight < ourHeight - MAXIMUM_COMMON_DELTA) {
|
||||||
LOGGER.info(String.format("Blockchain too divergent with peer %s", peer));
|
LOGGER.info(String.format("Blockchain too divergent with peer %s", peer));
|
||||||
|
peer.setLastTooDivergentTime(NTP.getTime());
|
||||||
return SynchronizationResult.TOO_DIVERGENT;
|
return SynchronizationResult.TOO_DIVERGENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1127,6 +1131,9 @@ public class Synchronizer extends Thread {
|
|||||||
testHeight = Math.max(testHeight - step, 1);
|
testHeight = Math.max(testHeight - step, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Peer not considered too divergent
|
||||||
|
peer.setLastTooDivergentTime(0L);
|
||||||
|
|
||||||
// Prepend test block's summary as first block summary, as summaries returned are *after* test block
|
// Prepend test block's summary as first block summary, as summaries returned are *after* test block
|
||||||
BlockSummaryData testBlockSummary = new BlockSummaryData(testBlockData);
|
BlockSummaryData testBlockSummary = new BlockSummaryData(testBlockData);
|
||||||
blockSummariesFromCommon.add(0, testBlockSummary);
|
blockSummariesFromCommon.add(0, testBlockSummary);
|
||||||
|
@ -82,7 +82,7 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Use a fixed thread pool to execute the arbitrary data file requests
|
// Use a fixed thread pool to execute the arbitrary data file requests
|
||||||
int threadCount = 10;
|
int threadCount = 5;
|
||||||
ExecutorService arbitraryDataFileRequestExecutor = Executors.newFixedThreadPool(threadCount);
|
ExecutorService arbitraryDataFileRequestExecutor = Executors.newFixedThreadPool(threadCount);
|
||||||
for (int i = 0; i < threadCount; i++) {
|
for (int i = 0; i < threadCount; i++) {
|
||||||
arbitraryDataFileRequestExecutor.execute(new ArbitraryDataFileRequestThread());
|
arbitraryDataFileRequestExecutor.execute(new ArbitraryDataFileRequestThread());
|
||||||
@ -288,7 +288,7 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
// The ID needs to match that of the original request
|
// The ID needs to match that of the original request
|
||||||
message.setId(originalMessage.getId());
|
message.setId(originalMessage.getId());
|
||||||
|
|
||||||
if (!requestingPeer.sendMessage(message)) {
|
if (!requestingPeer.sendMessageWithTimeout(message, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT)) {
|
||||||
LOGGER.debug("Failed to forward arbitrary data file to peer {}", requestingPeer);
|
LOGGER.debug("Failed to forward arbitrary data file to peer {}", requestingPeer);
|
||||||
requestingPeer.disconnect("failed to forward arbitrary data file");
|
requestingPeer.disconnect("failed to forward arbitrary data file");
|
||||||
}
|
}
|
||||||
@ -564,14 +564,17 @@ public class ArbitraryDataFileManager extends Thread {
|
|||||||
LOGGER.trace("Hash {} exists", hash58);
|
LOGGER.trace("Hash {} exists", hash58);
|
||||||
|
|
||||||
// We can serve the file directly as we already have it
|
// We can serve the file directly as we already have it
|
||||||
|
LOGGER.debug("Sending file {}...", arbitraryDataFile);
|
||||||
ArbitraryDataFileMessage arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, arbitraryDataFile);
|
ArbitraryDataFileMessage arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, arbitraryDataFile);
|
||||||
arbitraryDataFileMessage.setId(message.getId());
|
arbitraryDataFileMessage.setId(message.getId());
|
||||||
if (!peer.sendMessage(arbitraryDataFileMessage)) {
|
if (!peer.sendMessageWithTimeout(arbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT)) {
|
||||||
LOGGER.debug("Couldn't sent file");
|
LOGGER.debug("Couldn't send file {}", arbitraryDataFile);
|
||||||
peer.disconnect("failed to send file");
|
peer.disconnect("failed to send file");
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
LOGGER.debug("Sent file {}", arbitraryDataFile);
|
LOGGER.debug("Sent file {}", arbitraryDataFile);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else if (relayInfo != null) {
|
else if (relayInfo != null) {
|
||||||
LOGGER.debug("We have relay info for hash {}", Base58.encode(hash));
|
LOGGER.debug("We have relay info for hash {}", Base58.encode(hash));
|
||||||
// We need to ask this peer for the file
|
// We need to ask this peer for the file
|
||||||
|
@ -42,6 +42,7 @@ public class AtStatesPruner implements Runnable {
|
|||||||
|
|
||||||
repository.discardChanges();
|
repository.discardChanges();
|
||||||
repository.getATRepository().rebuildLatestAtStates();
|
repository.getATRepository().rebuildLatestAtStates();
|
||||||
|
repository.saveChanges();
|
||||||
|
|
||||||
while (!Controller.isStopping()) {
|
while (!Controller.isStopping()) {
|
||||||
repository.discardChanges();
|
repository.discardChanges();
|
||||||
|
@ -29,6 +29,7 @@ public class AtStatesTrimmer implements Runnable {
|
|||||||
|
|
||||||
repository.discardChanges();
|
repository.discardChanges();
|
||||||
repository.getATRepository().rebuildLatestAtStates();
|
repository.getATRepository().rebuildLatestAtStates();
|
||||||
|
repository.saveChanges();
|
||||||
|
|
||||||
while (!Controller.isStopping()) {
|
while (!Controller.isStopping()) {
|
||||||
repository.discardChanges();
|
repository.discardChanges();
|
||||||
|
@ -19,6 +19,7 @@ import org.qortal.data.transaction.MessageTransactionData;
|
|||||||
import org.qortal.group.Group;
|
import org.qortal.group.Group;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.repository.RepositoryManager;
|
||||||
import org.qortal.transaction.DeployAtTransaction;
|
import org.qortal.transaction.DeployAtTransaction;
|
||||||
import org.qortal.transaction.MessageTransaction;
|
import org.qortal.transaction.MessageTransaction;
|
||||||
import org.qortal.transaction.Transaction.ValidationResult;
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
@ -317,20 +318,27 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
|
|||||||
|
|
||||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||||
if (!isMessageAlreadySent) {
|
if (!isMessageAlreadySent) {
|
||||||
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
// Do this in a new thread so caller doesn't have to wait for computeNonce()
|
||||||
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
// In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded
|
||||||
|
new Thread(() -> {
|
||||||
|
try (final Repository threadsRepository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey());
|
||||||
|
MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
|
||||||
|
|
||||||
messageTransaction.computeNonce();
|
messageTransaction.computeNonce();
|
||||||
messageTransaction.sign(sender);
|
messageTransaction.sign(sender);
|
||||||
|
|
||||||
// reset repository state to prevent deadlock
|
// reset repository state to prevent deadlock
|
||||||
repository.discardChanges();
|
threadsRepository.discardChanges();
|
||||||
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
ValidationResult result = messageTransaction.importAsUnconfirmed();
|
||||||
|
|
||||||
if (result != ValidationResult.OK) {
|
if (result != ValidationResult.OK) {
|
||||||
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
|
||||||
return ResponseResult.NETWORK_ISSUE;
|
|
||||||
}
|
}
|
||||||
|
} catch (DataException e) {
|
||||||
|
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage()));
|
||||||
|
}
|
||||||
|
}, "TradeBot response").start();
|
||||||
}
|
}
|
||||||
|
|
||||||
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
|
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
|
||||||
|
@ -357,19 +357,33 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
* @return unspent BTC balance, or null if unable to determine balance
|
* @return unspent BTC balance, or null if unable to determine balance
|
||||||
*/
|
*/
|
||||||
public Long getWalletBalance(String key58) throws ForeignBlockchainException {
|
public Long getWalletBalance(String key58) throws ForeignBlockchainException {
|
||||||
// It's more accurate to calculate the balance from the transactions, rather than asking Bitcoinj
|
Long balance = 0L;
|
||||||
return this.getWalletBalanceFromTransactions(key58);
|
|
||||||
|
|
||||||
// Context.propagate(bitcoinjContext);
|
List<TransactionOutput> allUnspentOutputs = new ArrayList<>();
|
||||||
//
|
Set<String> walletAddresses = this.getWalletAddresses(key58);
|
||||||
// Wallet wallet = walletFromDeterministicKey58(key58);
|
for (String address : walletAddresses) {
|
||||||
// wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
|
allUnspentOutputs.addAll(this.getUnspentOutputs(address));
|
||||||
//
|
}
|
||||||
// Coin balance = wallet.getBalance();
|
for (TransactionOutput output : allUnspentOutputs) {
|
||||||
// if (balance == null)
|
if (!output.isAvailableForSpending()) {
|
||||||
// return null;
|
continue;
|
||||||
//
|
}
|
||||||
// return balance.value;
|
balance += output.getValue().value;
|
||||||
|
}
|
||||||
|
return balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getWalletBalanceFromBitcoinj(String key58) {
|
||||||
|
Context.propagate(bitcoinjContext);
|
||||||
|
|
||||||
|
Wallet wallet = walletFromDeterministicKey58(key58);
|
||||||
|
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
|
||||||
|
|
||||||
|
Coin balance = wallet.getBalance();
|
||||||
|
if (balance == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return balance.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
|
public Long getWalletBalanceFromTransactions(String key58) throws ForeignBlockchainException {
|
||||||
@ -464,6 +478,64 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<String> getWalletAddresses(String key58) throws ForeignBlockchainException {
|
||||||
|
synchronized (this) {
|
||||||
|
Context.propagate(bitcoinjContext);
|
||||||
|
|
||||||
|
Wallet wallet = walletFromDeterministicKey58(key58);
|
||||||
|
DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
|
||||||
|
|
||||||
|
keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
|
||||||
|
keyChain.maybeLookAhead();
|
||||||
|
|
||||||
|
List<DeterministicKey> keys = new ArrayList<>(keyChain.getLeafKeys());
|
||||||
|
|
||||||
|
Set<String> keySet = new HashSet<>();
|
||||||
|
|
||||||
|
int unusedCounter = 0;
|
||||||
|
int ki = 0;
|
||||||
|
do {
|
||||||
|
boolean areAllKeysUnused = true;
|
||||||
|
|
||||||
|
for (; ki < keys.size(); ++ki) {
|
||||||
|
DeterministicKey dKey = keys.get(ki);
|
||||||
|
|
||||||
|
// Check for transactions
|
||||||
|
Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
|
||||||
|
keySet.add(address.toString());
|
||||||
|
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||||
|
|
||||||
|
// Ask for transaction history - if it's empty then key has never been used
|
||||||
|
List<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, false);
|
||||||
|
|
||||||
|
if (!historicTransactionHashes.isEmpty()) {
|
||||||
|
areAllKeysUnused = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areAllKeysUnused) {
|
||||||
|
// No transactions
|
||||||
|
if (unusedCounter >= Settings.getInstance().getGapLimit()) {
|
||||||
|
// ... and we've hit our search limit
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// We haven't hit our search limit yet so increment the counter and keep looking
|
||||||
|
unusedCounter += WALLET_KEY_LOOKAHEAD_INCREMENT;
|
||||||
|
} else {
|
||||||
|
// Some keys in this batch were used, so reset the counter
|
||||||
|
unusedCounter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate some more keys
|
||||||
|
keys.addAll(generateMoreKeys(keyChain));
|
||||||
|
|
||||||
|
// Process new keys
|
||||||
|
} while (true);
|
||||||
|
|
||||||
|
return keySet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) {
|
protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) {
|
||||||
long amount = 0;
|
long amount = 0;
|
||||||
long total = 0L;
|
long total = 0L;
|
||||||
|
@ -134,6 +134,8 @@ public class Digibyte extends Bitcoiny {
|
|||||||
Context bitcoinjContext = new Context(digibyteNet.getParams());
|
Context bitcoinjContext = new Context(digibyteNet.getParams());
|
||||||
|
|
||||||
instance = new Digibyte(digibyteNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
instance = new Digibyte(digibyteNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
|
||||||
|
electrumX.setBlockchain(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
@ -40,7 +40,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
|
|||||||
private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported";
|
private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported";
|
||||||
|
|
||||||
private static final int RESPONSE_TIME_READINGS = 5;
|
private static final int RESPONSE_TIME_READINGS = 5;
|
||||||
private static final long MAX_AVG_RESPONSE_TIME = 500L; // ms
|
private static final long MAX_AVG_RESPONSE_TIME = 1000L; // ms
|
||||||
|
|
||||||
public static class Server {
|
public static class Server {
|
||||||
String hostname;
|
String hostname;
|
||||||
|
@ -117,7 +117,7 @@ public class PirateWallet {
|
|||||||
// Restore existing wallet
|
// Restore existing wallet
|
||||||
String response = LiteWalletJni.initfromb64(serverUri, params, wallet, saplingOutput64, saplingSpend64);
|
String response = LiteWalletJni.initfromb64(serverUri, params, wallet, saplingOutput64, saplingSpend64);
|
||||||
if (response != null && !response.contains("\"initalized\":true")) {
|
if (response != null && !response.contains("\"initalized\":true")) {
|
||||||
LOGGER.info("Unable to initialize Pirate Chain wallet: {}", response);
|
LOGGER.info("Unable to initialize Pirate Chain wallet at {}: {}", serverUri, response);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this.seedPhrase = inputSeedPhrase;
|
this.seedPhrase = inputSeedPhrase;
|
||||||
|
@ -138,6 +138,8 @@ public class Ravencoin extends Bitcoiny {
|
|||||||
Context bitcoinjContext = new Context(ravencoinNet.getParams());
|
Context bitcoinjContext = new Context(ravencoinNet.getParams());
|
||||||
|
|
||||||
instance = new Ravencoin(ravencoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
instance = new Ravencoin(ravencoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
|
||||||
|
|
||||||
|
electrumX.setBlockchain(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
|
@ -18,6 +18,7 @@ public class AccountData {
|
|||||||
protected int level;
|
protected int level;
|
||||||
protected int blocksMinted;
|
protected int blocksMinted;
|
||||||
protected int blocksMintedAdjustment;
|
protected int blocksMintedAdjustment;
|
||||||
|
protected int blocksMintedPenalty;
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ public class AccountData {
|
|||||||
protected AccountData() {
|
protected AccountData() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, int level, int blocksMinted, int blocksMintedAdjustment) {
|
public AccountData(String address, byte[] reference, byte[] publicKey, int defaultGroupId, int flags, int level, int blocksMinted, int blocksMintedAdjustment, int blocksMintedPenalty) {
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.reference = reference;
|
this.reference = reference;
|
||||||
this.publicKey = publicKey;
|
this.publicKey = publicKey;
|
||||||
@ -34,10 +35,11 @@ public class AccountData {
|
|||||||
this.level = level;
|
this.level = level;
|
||||||
this.blocksMinted = blocksMinted;
|
this.blocksMinted = blocksMinted;
|
||||||
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
||||||
|
this.blocksMintedPenalty = blocksMintedPenalty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccountData(String address) {
|
public AccountData(String address) {
|
||||||
this(address, null, null, Group.NO_GROUP, 0, 0, 0, 0);
|
this(address, null, null, Group.NO_GROUP, 0, 0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters/Setters
|
// Getters/Setters
|
||||||
@ -102,6 +104,14 @@ public class AccountData {
|
|||||||
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
this.blocksMintedAdjustment = blocksMintedAdjustment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getBlocksMintedPenalty() {
|
||||||
|
return this.blocksMintedPenalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlocksMintedPenalty(int blocksMintedPenalty) {
|
||||||
|
this.blocksMintedPenalty = blocksMintedPenalty;
|
||||||
|
}
|
||||||
|
|
||||||
// Comparison
|
// Comparison
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
package org.qortal.data.account;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
|
||||||
|
// All properties to be converted to JSON via JAXB
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class AccountPenaltyData {
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
private String address;
|
||||||
|
private int blocksMintedPenalty;
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
// necessary for JAXB
|
||||||
|
protected AccountPenaltyData() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountPenaltyData(String address, int blocksMintedPenalty) {
|
||||||
|
this.address = address;
|
||||||
|
this.blocksMintedPenalty = blocksMintedPenalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters/Setters
|
||||||
|
|
||||||
|
public String getAddress() {
|
||||||
|
return this.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBlocksMintedPenalty() {
|
||||||
|
return this.blocksMintedPenalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return String.format("%s has penalty %d", this.address, this.blocksMintedPenalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object b) {
|
||||||
|
if (!(b instanceof AccountPenaltyData))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return this.getAddress().equals(((AccountPenaltyData) b).getAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return address.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -27,6 +27,8 @@ public class ChatMessage {
|
|||||||
|
|
||||||
private String recipientName;
|
private String recipientName;
|
||||||
|
|
||||||
|
private byte[] chatReference;
|
||||||
|
|
||||||
private byte[] data;
|
private byte[] data;
|
||||||
|
|
||||||
private boolean isText;
|
private boolean isText;
|
||||||
@ -42,8 +44,8 @@ public class ChatMessage {
|
|||||||
|
|
||||||
// For repository use
|
// For repository use
|
||||||
public ChatMessage(long timestamp, int txGroupId, byte[] reference, byte[] senderPublicKey, String sender,
|
public ChatMessage(long timestamp, int txGroupId, byte[] reference, byte[] senderPublicKey, String sender,
|
||||||
String senderName, String recipient, String recipientName, byte[] data, boolean isText,
|
String senderName, String recipient, String recipientName, byte[] chatReference, byte[] data,
|
||||||
boolean isEncrypted, byte[] signature) {
|
boolean isText, boolean isEncrypted, byte[] signature) {
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.txGroupId = txGroupId;
|
this.txGroupId = txGroupId;
|
||||||
this.reference = reference;
|
this.reference = reference;
|
||||||
@ -52,6 +54,7 @@ public class ChatMessage {
|
|||||||
this.senderName = senderName;
|
this.senderName = senderName;
|
||||||
this.recipient = recipient;
|
this.recipient = recipient;
|
||||||
this.recipientName = recipientName;
|
this.recipientName = recipientName;
|
||||||
|
this.chatReference = chatReference;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.isText = isText;
|
this.isText = isText;
|
||||||
this.isEncrypted = isEncrypted;
|
this.isEncrypted = isEncrypted;
|
||||||
@ -90,6 +93,10 @@ public class ChatMessage {
|
|||||||
return this.recipientName;
|
return this.recipientName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getChatReference() {
|
||||||
|
return this.chatReference;
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] getData() {
|
public byte[] getData() {
|
||||||
return this.data;
|
return this.data;
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,8 @@ public class ChatTransactionData extends TransactionData {
|
|||||||
|
|
||||||
private String recipient; // can be null
|
private String recipient; // can be null
|
||||||
|
|
||||||
|
private byte[] chatReference; // can be null
|
||||||
|
|
||||||
@Schema(description = "raw message data, possibly UTF8 text", example = "2yGEbwRFyhPZZckKA")
|
@Schema(description = "raw message data, possibly UTF8 text", example = "2yGEbwRFyhPZZckKA")
|
||||||
private byte[] data;
|
private byte[] data;
|
||||||
|
|
||||||
@ -44,13 +46,14 @@ public class ChatTransactionData extends TransactionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ChatTransactionData(BaseTransactionData baseTransactionData,
|
public ChatTransactionData(BaseTransactionData baseTransactionData,
|
||||||
String sender, int nonce, String recipient, byte[] data, boolean isText, boolean isEncrypted) {
|
String sender, int nonce, String recipient, byte[] chatReference, byte[] data, boolean isText, boolean isEncrypted) {
|
||||||
super(TransactionType.CHAT, baseTransactionData);
|
super(TransactionType.CHAT, baseTransactionData);
|
||||||
|
|
||||||
this.senderPublicKey = baseTransactionData.creatorPublicKey;
|
this.senderPublicKey = baseTransactionData.creatorPublicKey;
|
||||||
this.sender = sender;
|
this.sender = sender;
|
||||||
this.nonce = nonce;
|
this.nonce = nonce;
|
||||||
this.recipient = recipient;
|
this.recipient = recipient;
|
||||||
|
this.chatReference = chatReference;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.isText = isText;
|
this.isText = isText;
|
||||||
this.isEncrypted = isEncrypted;
|
this.isEncrypted = isEncrypted;
|
||||||
@ -78,6 +81,14 @@ public class ChatTransactionData extends TransactionData {
|
|||||||
return this.recipient;
|
return this.recipient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getChatReference() {
|
||||||
|
return this.chatReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChatReference(byte[] chatReference) {
|
||||||
|
this.chatReference = chatReference;
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] getData() {
|
public byte[] getData() {
|
||||||
return this.data;
|
return this.data;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package org.qortal.data.transaction;
|
|||||||
|
|
||||||
import javax.xml.bind.annotation.XmlAccessType;
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
import javax.xml.bind.annotation.XmlAccessorType;
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.XmlElement;
|
||||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||||
|
|
||||||
import org.qortal.transaction.Transaction.TransactionType;
|
import org.qortal.transaction.Transaction.TransactionType;
|
||||||
@ -90,4 +91,17 @@ public class DeployAtTransactionData extends TransactionData {
|
|||||||
this.aTAddress = AtAddress;
|
this.aTAddress = AtAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-expose creatorPublicKey for this transaction type for JAXB
|
||||||
|
@XmlElement(name = "creatorPublicKey")
|
||||||
|
@Schema(name = "creatorPublicKey", description = "AT creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
|
||||||
|
public byte[] getAtCreatorPublicKey() {
|
||||||
|
return this.creatorPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@XmlElement(name = "creatorPublicKey")
|
||||||
|
@Schema(name = "creatorPublicKey", description = "AT creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
|
||||||
|
public void setAtCreatorPublicKey(byte[] creatorPublicKey) {
|
||||||
|
this.creatorPublicKey = creatorPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -128,6 +128,10 @@ public abstract class TransactionData {
|
|||||||
return this.txGroupId;
|
return this.txGroupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTxGroupId(int txGroupId) {
|
||||||
|
this.txGroupId = txGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] getReference() {
|
public byte[] getReference() {
|
||||||
return this.reference;
|
return this.reference;
|
||||||
}
|
}
|
||||||
|
@ -80,6 +80,9 @@ public class Group {
|
|||||||
// Useful constants
|
// Useful constants
|
||||||
public static final int NO_GROUP = 0;
|
public static final int NO_GROUP = 0;
|
||||||
|
|
||||||
|
// Null owner address corresponds with public key "11111111111111111111111111111111"
|
||||||
|
public static String NULL_OWNER_ADDRESS = "QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG";
|
||||||
|
|
||||||
public static final int MIN_NAME_SIZE = 3;
|
public static final int MIN_NAME_SIZE = 3;
|
||||||
public static final int MAX_NAME_SIZE = 32;
|
public static final int MAX_NAME_SIZE = 32;
|
||||||
public static final int MAX_DESCRIPTION_SIZE = 128;
|
public static final int MAX_DESCRIPTION_SIZE = 128;
|
||||||
|
@ -265,7 +265,7 @@ public enum Handshake {
|
|||||||
private static final long PEER_VERSION_131 = 0x0100030001L;
|
private static final long PEER_VERSION_131 = 0x0100030001L;
|
||||||
|
|
||||||
/** Minimum peer version that we are allowed to communicate with */
|
/** Minimum peer version that we are allowed to communicate with */
|
||||||
private static final String MIN_PEER_VERSION = "3.1.0";
|
private static final String MIN_PEER_VERSION = "3.8.2";
|
||||||
|
|
||||||
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
|
private static final int POW_BUFFER_SIZE_PRE_131 = 8 * 1024 * 1024; // bytes
|
||||||
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits
|
private static final int POW_DIFFICULTY_PRE_131 = 8; // leading zero bits
|
||||||
|
@ -155,6 +155,11 @@ public class Peer {
|
|||||||
*/
|
*/
|
||||||
private CommonBlockData commonBlockData;
|
private CommonBlockData commonBlockData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last time we detected this peer as TOO_DIVERGENT
|
||||||
|
*/
|
||||||
|
private Long lastTooDivergentTime;
|
||||||
|
|
||||||
// Message stats
|
// Message stats
|
||||||
|
|
||||||
private static class MessageStats {
|
private static class MessageStats {
|
||||||
@ -383,6 +388,14 @@ public class Peer {
|
|||||||
this.commonBlockData = commonBlockData;
|
this.commonBlockData = commonBlockData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getLastTooDivergentTime() {
|
||||||
|
return this.lastTooDivergentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastTooDivergentTime(Long lastTooDivergentTime) {
|
||||||
|
this.lastTooDivergentTime = lastTooDivergentTime;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSyncInProgress() {
|
public boolean isSyncInProgress() {
|
||||||
return this.syncInProgress;
|
return this.syncInProgress;
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,8 @@ public class AccountMessage extends Message {
|
|||||||
|
|
||||||
bytes.write(Ints.toByteArray(accountData.getBlocksMintedAdjustment()));
|
bytes.write(Ints.toByteArray(accountData.getBlocksMintedAdjustment()));
|
||||||
|
|
||||||
|
bytes.write(Ints.toByteArray(accountData.getBlocksMintedPenalty()));
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||||
}
|
}
|
||||||
@ -80,7 +82,9 @@ public class AccountMessage extends Message {
|
|||||||
|
|
||||||
int blocksMintedAdjustment = byteBuffer.getInt();
|
int blocksMintedAdjustment = byteBuffer.getInt();
|
||||||
|
|
||||||
AccountData accountData = new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment);
|
int blocksMintedPenalty = byteBuffer.getInt();
|
||||||
|
|
||||||
|
AccountData accountData = new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty);
|
||||||
return new AccountMessage(id, accountData);
|
return new AccountMessage(id, accountData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
package org.qortal.repository;
|
package org.qortal.repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.qortal.data.account.AccountBalanceData;
|
import org.qortal.data.account.*;
|
||||||
import org.qortal.data.account.AccountData;
|
|
||||||
import org.qortal.data.account.EligibleQoraHolderData;
|
|
||||||
import org.qortal.data.account.MintingAccountData;
|
|
||||||
import org.qortal.data.account.QortFromQoraData;
|
|
||||||
import org.qortal.data.account.RewardShareData;
|
|
||||||
|
|
||||||
public interface AccountRepository {
|
public interface AccountRepository {
|
||||||
|
|
||||||
@ -19,6 +15,9 @@ public interface AccountRepository {
|
|||||||
/** Returns accounts with <b>any</b> bit set in given mask. */
|
/** Returns accounts with <b>any</b> bit set in given mask. */
|
||||||
public List<AccountData> getFlaggedAccounts(int mask) throws DataException;
|
public List<AccountData> getFlaggedAccounts(int mask) throws DataException;
|
||||||
|
|
||||||
|
/** Returns accounts with a blockedMintedPenalty */
|
||||||
|
public List<AccountData> getPenaltyAccounts() throws DataException;
|
||||||
|
|
||||||
/** Returns account's last reference or null if not set or account not found. */
|
/** Returns account's last reference or null if not set or account not found. */
|
||||||
public byte[] getLastReference(String address) throws DataException;
|
public byte[] getLastReference(String address) throws DataException;
|
||||||
|
|
||||||
@ -100,6 +99,18 @@ public interface AccountRepository {
|
|||||||
*/
|
*/
|
||||||
public void modifyMintedBlockCounts(List<String> addresses, int delta) throws DataException;
|
public void modifyMintedBlockCounts(List<String> addresses, int delta) throws DataException;
|
||||||
|
|
||||||
|
/** Returns account's block minted penalty count or null if account not found. */
|
||||||
|
public Integer getBlocksMintedPenaltyCount(String address) throws DataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets blocks minted penalties for given list of accounts.
|
||||||
|
* This replaces the existing values rather than modifying them by a delta.
|
||||||
|
*
|
||||||
|
* @param accountPenalties
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public void updateBlocksMintedPenalties(Set<AccountPenaltyData> accountPenalties) throws DataException;
|
||||||
|
|
||||||
/** Delete account from repository. */
|
/** Delete account from repository. */
|
||||||
public void delete(String address) throws DataException;
|
public void delete(String address) throws DataException;
|
||||||
|
|
||||||
|
@ -14,8 +14,8 @@ public interface ChatRepository {
|
|||||||
* Expects EITHER non-null txGroupID OR non-null sender and recipient addresses.
|
* Expects EITHER non-null txGroupID OR non-null sender and recipient addresses.
|
||||||
*/
|
*/
|
||||||
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after,
|
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after,
|
||||||
Integer txGroupId, byte[] reference, List<String> involving,
|
Integer txGroupId, byte[] reference, byte[] chatReferenceBytes, Boolean hasChatReference,
|
||||||
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
List<String> involving, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||||
|
|
||||||
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData) throws DataException;
|
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData) throws DataException;
|
||||||
|
|
||||||
|
@ -131,7 +131,14 @@ public interface GroupRepository {
|
|||||||
|
|
||||||
public GroupBanData getBan(int groupId, String member) throws DataException;
|
public GroupBanData getBan(int groupId, String member) throws DataException;
|
||||||
|
|
||||||
public boolean banExists(int groupId, String offender) throws DataException;
|
/**
|
||||||
|
* IMPORTANT: when using banExists() as part of validation, the timestamp must be that of the transaction that
|
||||||
|
* is calling banExists() as part of its validation. It must NOT be the current time, unless this is being
|
||||||
|
* called outside of validation, as part of an on demand check for a ban existing (such as via an API call).
|
||||||
|
* This is because we need to evaluate a ban's status based on the time of the subsequent transaction, as
|
||||||
|
* validation will not occur at a fixed time for every node. For some, it could be months into the future.
|
||||||
|
*/
|
||||||
|
public boolean banExists(int groupId, String offender, long timestamp) throws DataException;
|
||||||
|
|
||||||
public List<GroupBanData> getGroupBans(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
public List<GroupBanData> getGroupBans(int groupId, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||||
|
|
||||||
|
@ -179,6 +179,15 @@ public interface TransactionRepository {
|
|||||||
public List<TransferAssetTransactionData> getAssetTransfers(long assetId, String address, Integer limit, Integer offset, Boolean reverse)
|
public List<TransferAssetTransactionData> getAssetTransfers(long assetId, String address, Integer limit, Integer offset, Boolean reverse)
|
||||||
throws DataException;
|
throws DataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of reward share transaction creators, excluding self shares.
|
||||||
|
* This uses confirmed transactions only.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public List<String> getConfirmedRewardShareCreatorsExcludingSelfShares() throws DataException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns list of transactions pending approval, with optional txGgroupId filtering.
|
* Returns list of transactions pending approval, with optional txGgroupId filtering.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -6,15 +6,11 @@ import java.sql.ResultSet;
|
|||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.data.account.AccountBalanceData;
|
import org.qortal.data.account.*;
|
||||||
import org.qortal.data.account.AccountData;
|
|
||||||
import org.qortal.data.account.EligibleQoraHolderData;
|
|
||||||
import org.qortal.data.account.MintingAccountData;
|
|
||||||
import org.qortal.data.account.QortFromQoraData;
|
|
||||||
import org.qortal.data.account.RewardShareData;
|
|
||||||
import org.qortal.repository.AccountRepository;
|
import org.qortal.repository.AccountRepository;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
|
|
||||||
@ -30,7 +26,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AccountData getAccount(String address) throws DataException {
|
public AccountData getAccount(String address) throws DataException {
|
||||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment FROM Accounts WHERE account = ?";
|
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty FROM Accounts WHERE account = ?";
|
||||||
|
|
||||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
|
try (ResultSet resultSet = this.repository.checkedExecute(sql, address)) {
|
||||||
if (resultSet == null)
|
if (resultSet == null)
|
||||||
@ -43,8 +39,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
int level = resultSet.getInt(5);
|
int level = resultSet.getInt(5);
|
||||||
int blocksMinted = resultSet.getInt(6);
|
int blocksMinted = resultSet.getInt(6);
|
||||||
int blocksMintedAdjustment = resultSet.getInt(7);
|
int blocksMintedAdjustment = resultSet.getInt(7);
|
||||||
|
int blocksMintedPenalty = resultSet.getInt(8);
|
||||||
|
|
||||||
return new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment);
|
return new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to fetch account info from repository", e);
|
throw new DataException("Unable to fetch account info from repository", e);
|
||||||
}
|
}
|
||||||
@ -52,7 +49,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AccountData> getFlaggedAccounts(int mask) throws DataException {
|
public List<AccountData> getFlaggedAccounts(int mask) throws DataException {
|
||||||
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, account FROM Accounts WHERE BITAND(flags, ?) != 0";
|
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty, account FROM Accounts WHERE BITAND(flags, ?) != 0";
|
||||||
|
|
||||||
List<AccountData> accounts = new ArrayList<>();
|
List<AccountData> accounts = new ArrayList<>();
|
||||||
|
|
||||||
@ -68,9 +65,10 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
int level = resultSet.getInt(5);
|
int level = resultSet.getInt(5);
|
||||||
int blocksMinted = resultSet.getInt(6);
|
int blocksMinted = resultSet.getInt(6);
|
||||||
int blocksMintedAdjustment = resultSet.getInt(7);
|
int blocksMintedAdjustment = resultSet.getInt(7);
|
||||||
String address = resultSet.getString(8);
|
int blocksMintedPenalty = resultSet.getInt(8);
|
||||||
|
String address = resultSet.getString(9);
|
||||||
|
|
||||||
accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment));
|
accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty));
|
||||||
} while (resultSet.next());
|
} while (resultSet.next());
|
||||||
|
|
||||||
return accounts;
|
return accounts;
|
||||||
@ -79,6 +77,36 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AccountData> getPenaltyAccounts() throws DataException {
|
||||||
|
String sql = "SELECT reference, public_key, default_group_id, flags, level, blocks_minted, blocks_minted_adjustment, blocks_minted_penalty, account FROM Accounts WHERE blocks_minted_penalty != 0";
|
||||||
|
|
||||||
|
List<AccountData> accounts = new ArrayList<>();
|
||||||
|
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return accounts;
|
||||||
|
|
||||||
|
do {
|
||||||
|
byte[] reference = resultSet.getBytes(1);
|
||||||
|
byte[] publicKey = resultSet.getBytes(2);
|
||||||
|
int defaultGroupId = resultSet.getInt(3);
|
||||||
|
int flags = resultSet.getInt(4);
|
||||||
|
int level = resultSet.getInt(5);
|
||||||
|
int blocksMinted = resultSet.getInt(6);
|
||||||
|
int blocksMintedAdjustment = resultSet.getInt(7);
|
||||||
|
int blocksMintedPenalty = resultSet.getInt(8);
|
||||||
|
String address = resultSet.getString(9);
|
||||||
|
|
||||||
|
accounts.add(new AccountData(address, reference, publicKey, defaultGroupId, flags, level, blocksMinted, blocksMintedAdjustment, blocksMintedPenalty));
|
||||||
|
} while (resultSet.next());
|
||||||
|
|
||||||
|
return accounts;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch penalty accounts from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] getLastReference(String address) throws DataException {
|
public byte[] getLastReference(String address) throws DataException {
|
||||||
String sql = "SELECT reference FROM Accounts WHERE account = ?";
|
String sql = "SELECT reference FROM Accounts WHERE account = ?";
|
||||||
@ -298,6 +326,39 @@ public class HSQLDBAccountRepository implements AccountRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer getBlocksMintedPenaltyCount(String address) throws DataException {
|
||||||
|
String sql = "SELECT blocks_minted_penalty 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 block minted penalty count from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void updateBlocksMintedPenalties(Set<AccountPenaltyData> accountPenalties) throws DataException {
|
||||||
|
// Nothing to do?
|
||||||
|
if (accountPenalties == null || accountPenalties.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Map balance changes into SQL bind params, filtering out no-op changes
|
||||||
|
List<Object[]> updateBlocksMintedPenaltyParams = accountPenalties.stream()
|
||||||
|
.map(accountPenalty -> new Object[] { accountPenalty.getAddress(), accountPenalty.getBlocksMintedPenalty(), accountPenalty.getBlocksMintedPenalty() })
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Perform actual balance changes
|
||||||
|
String sql = "INSERT INTO Accounts (account, blocks_minted_penalty) VALUES (?, ?) " +
|
||||||
|
"ON DUPLICATE KEY UPDATE blocks_minted_penalty = blocks_minted_penalty + ?";
|
||||||
|
try {
|
||||||
|
this.repository.executeCheckedBatchUpdate(sql, updateBlocksMintedPenaltyParams);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to set blocks minted penalties in repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(String address) throws DataException {
|
public void delete(String address) throws DataException {
|
||||||
// NOTE: Account balances are deleted automatically by the database thanks to "ON DELETE CASCADE" in AccountBalances' FOREIGN KEY
|
// NOTE: Account balances are deleted automatically by the database thanks to "ON DELETE CASCADE" in AccountBalances' FOREIGN KEY
|
||||||
|
@ -24,8 +24,8 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId, byte[] referenceBytes,
|
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId, byte[] referenceBytes,
|
||||||
List<String> involving, Integer limit, Integer offset, Boolean reverse)
|
byte[] chatReferenceBytes, Boolean hasChatReference, List<String> involving,
|
||||||
throws DataException {
|
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||||
// Check args meet expectations
|
// Check args meet expectations
|
||||||
if ((txGroupId != null && involving != null && !involving.isEmpty())
|
if ((txGroupId != null && involving != null && !involving.isEmpty())
|
||||||
|| (txGroupId == null && (involving == null || involving.size() != 2)))
|
|| (txGroupId == null && (involving == null || involving.size() != 2)))
|
||||||
@ -35,7 +35,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
|
|
||||||
sql.append("SELECT created_when, tx_group_id, Transactions.reference, creator, "
|
sql.append("SELECT created_when, tx_group_id, Transactions.reference, creator, "
|
||||||
+ "sender, SenderNames.name, recipient, RecipientNames.name, "
|
+ "sender, SenderNames.name, recipient, RecipientNames.name, "
|
||||||
+ "data, is_text, is_encrypted, signature "
|
+ "chat_reference, data, is_text, is_encrypted, signature "
|
||||||
+ "FROM ChatTransactions "
|
+ "FROM ChatTransactions "
|
||||||
+ "JOIN Transactions USING (signature) "
|
+ "JOIN Transactions USING (signature) "
|
||||||
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
||||||
@ -62,6 +62,18 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
bindParams.add(referenceBytes);
|
bindParams.add(referenceBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (chatReferenceBytes != null) {
|
||||||
|
whereClauses.add("chat_reference = ?");
|
||||||
|
bindParams.add(chatReferenceBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChatReference != null && hasChatReference == true) {
|
||||||
|
whereClauses.add("chat_reference IS NOT NULL");
|
||||||
|
}
|
||||||
|
else if (hasChatReference != null && hasChatReference == false) {
|
||||||
|
whereClauses.add("chat_reference IS NULL");
|
||||||
|
}
|
||||||
|
|
||||||
if (txGroupId != null) {
|
if (txGroupId != null) {
|
||||||
whereClauses.add("tx_group_id = " + txGroupId); // int safe to use literally
|
whereClauses.add("tx_group_id = " + txGroupId); // int safe to use literally
|
||||||
whereClauses.add("recipient IS NULL");
|
whereClauses.add("recipient IS NULL");
|
||||||
@ -103,13 +115,14 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
String senderName = resultSet.getString(6);
|
String senderName = resultSet.getString(6);
|
||||||
String recipient = resultSet.getString(7);
|
String recipient = resultSet.getString(7);
|
||||||
String recipientName = resultSet.getString(8);
|
String recipientName = resultSet.getString(8);
|
||||||
byte[] data = resultSet.getBytes(9);
|
byte[] chatReference = resultSet.getBytes(9);
|
||||||
boolean isText = resultSet.getBoolean(10);
|
byte[] data = resultSet.getBytes(10);
|
||||||
boolean isEncrypted = resultSet.getBoolean(11);
|
boolean isText = resultSet.getBoolean(11);
|
||||||
byte[] signature = resultSet.getBytes(12);
|
boolean isEncrypted = resultSet.getBoolean(12);
|
||||||
|
byte[] signature = resultSet.getBytes(13);
|
||||||
|
|
||||||
ChatMessage chatMessage = new ChatMessage(timestamp, groupId, reference, senderPublicKey, sender,
|
ChatMessage chatMessage = new ChatMessage(timestamp, groupId, reference, senderPublicKey, sender,
|
||||||
senderName, recipient, recipientName, data, isText, isEncrypted, signature);
|
senderName, recipient, recipientName, chatReference, data, isText, isEncrypted, signature);
|
||||||
|
|
||||||
chatMessages.add(chatMessage);
|
chatMessages.add(chatMessage);
|
||||||
} while (resultSet.next());
|
} while (resultSet.next());
|
||||||
@ -141,13 +154,14 @@ public class HSQLDBChatRepository implements ChatRepository {
|
|||||||
byte[] senderPublicKey = chatTransactionData.getSenderPublicKey();
|
byte[] senderPublicKey = chatTransactionData.getSenderPublicKey();
|
||||||
String sender = chatTransactionData.getSender();
|
String sender = chatTransactionData.getSender();
|
||||||
String recipient = chatTransactionData.getRecipient();
|
String recipient = chatTransactionData.getRecipient();
|
||||||
|
byte[] chatReference = chatTransactionData.getChatReference();
|
||||||
byte[] data = chatTransactionData.getData();
|
byte[] data = chatTransactionData.getData();
|
||||||
boolean isText = chatTransactionData.getIsText();
|
boolean isText = chatTransactionData.getIsText();
|
||||||
boolean isEncrypted = chatTransactionData.getIsEncrypted();
|
boolean isEncrypted = chatTransactionData.getIsEncrypted();
|
||||||
byte[] signature = chatTransactionData.getSignature();
|
byte[] signature = chatTransactionData.getSignature();
|
||||||
|
|
||||||
return new ChatMessage(timestamp, groupId, reference, senderPublicKey, sender,
|
return new ChatMessage(timestamp, groupId, reference, senderPublicKey, sender,
|
||||||
senderName, recipient, recipientName, data, isText, isEncrypted, signature);
|
senderName, recipient, recipientName, chatReference, data, isText, isEncrypted, signature);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to fetch convert chat transaction from repository", e);
|
throw new DataException("Unable to fetch convert chat transaction from repository", e);
|
||||||
}
|
}
|
||||||
|
@ -975,6 +975,19 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN receiving_account_info SET DATA TYPE VARBINARY(128)");
|
stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN receiving_account_info SET DATA TYPE VARBINARY(128)");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 44:
|
||||||
|
// Add blocks minted penalty
|
||||||
|
stmt.execute("ALTER TABLE Accounts ADD blocks_minted_penalty INTEGER NOT NULL DEFAULT 0");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 45:
|
||||||
|
// Add a chat reference, to allow one message to reference another, and for this to be easily
|
||||||
|
// searchable. Null values are allowed as most transactions won't have a reference.
|
||||||
|
stmt.execute("ALTER TABLE ChatTransactions ADD chat_reference Signature");
|
||||||
|
// For finding chat messages by reference
|
||||||
|
stmt.execute("CREATE INDEX ChatTransactionsChatReferenceIndex ON ChatTransactions (chat_reference)");
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// nothing to do
|
// nothing to do
|
||||||
return false;
|
return false;
|
||||||
|
@ -777,9 +777,9 @@ public class HSQLDBGroupRepository implements GroupRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean banExists(int groupId, String offender) throws DataException {
|
public boolean banExists(int groupId, String offender, long timestamp) throws DataException {
|
||||||
try {
|
try {
|
||||||
return this.repository.exists("GroupBans", "group_id = ? AND offender = ?", groupId, offender);
|
return this.repository.exists("GroupBans", "group_id = ? AND offender = ? AND (expires_when IS NULL OR expires_when > ?)", groupId, offender, timestamp);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to check for group ban in repository", e);
|
throw new DataException("Unable to check for group ban in repository", e);
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ public class HSQLDBChatTransactionRepository extends HSQLDBTransactionRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
|
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
|
||||||
String sql = "SELECT sender, nonce, recipient, is_text, is_encrypted, data FROM ChatTransactions WHERE signature = ?";
|
String sql = "SELECT sender, nonce, recipient, is_text, is_encrypted, data, chat_reference FROM ChatTransactions WHERE signature = ?";
|
||||||
|
|
||||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
|
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
|
||||||
if (resultSet == null)
|
if (resultSet == null)
|
||||||
@ -29,8 +29,9 @@ public class HSQLDBChatTransactionRepository extends HSQLDBTransactionRepository
|
|||||||
boolean isText = resultSet.getBoolean(4);
|
boolean isText = resultSet.getBoolean(4);
|
||||||
boolean isEncrypted = resultSet.getBoolean(5);
|
boolean isEncrypted = resultSet.getBoolean(5);
|
||||||
byte[] data = resultSet.getBytes(6);
|
byte[] data = resultSet.getBytes(6);
|
||||||
|
byte[] chatReference = resultSet.getBytes(7);
|
||||||
|
|
||||||
return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, data, isText, isEncrypted);
|
return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, chatReference, data, isText, isEncrypted);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw new DataException("Unable to fetch chat transaction from repository", e);
|
throw new DataException("Unable to fetch chat transaction from repository", e);
|
||||||
}
|
}
|
||||||
@ -45,7 +46,7 @@ public class HSQLDBChatTransactionRepository extends HSQLDBTransactionRepository
|
|||||||
saveHelper.bind("signature", chatTransactionData.getSignature()).bind("nonce", chatTransactionData.getNonce())
|
saveHelper.bind("signature", chatTransactionData.getSignature()).bind("nonce", chatTransactionData.getNonce())
|
||||||
.bind("sender", chatTransactionData.getSender()).bind("recipient", chatTransactionData.getRecipient())
|
.bind("sender", chatTransactionData.getSender()).bind("recipient", chatTransactionData.getRecipient())
|
||||||
.bind("is_text", chatTransactionData.getIsText()).bind("is_encrypted", chatTransactionData.getIsEncrypted())
|
.bind("is_text", chatTransactionData.getIsText()).bind("is_encrypted", chatTransactionData.getIsEncrypted())
|
||||||
.bind("data", chatTransactionData.getData());
|
.bind("data", chatTransactionData.getData()).bind("chat_reference", chatTransactionData.getChatReference());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
saveHelper.execute(this.repository);
|
saveHelper.execute(this.repository);
|
||||||
|
@ -7,11 +7,7 @@ import java.lang.reflect.InvocationTargetException;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.EnumMap;
|
|
||||||
import java.util.EnumSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
@ -969,6 +965,33 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getConfirmedRewardShareCreatorsExcludingSelfShares() throws DataException {
|
||||||
|
List<String> rewardShareCreators = new ArrayList<>();
|
||||||
|
|
||||||
|
String sql = "SELECT account "
|
||||||
|
+ "FROM RewardShareTransactions "
|
||||||
|
+ "JOIN Accounts ON Accounts.public_key = RewardShareTransactions.minter_public_key "
|
||||||
|
+ "JOIN Transactions ON Transactions.signature = RewardShareTransactions.signature "
|
||||||
|
+ "WHERE block_height IS NOT NULL AND RewardShareTransactions.recipient != Accounts.account "
|
||||||
|
+ "GROUP BY account "
|
||||||
|
+ "ORDER BY account";
|
||||||
|
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return rewardShareCreators;
|
||||||
|
|
||||||
|
do {
|
||||||
|
String address = resultSet.getString(1);
|
||||||
|
|
||||||
|
rewardShareCreators.add(address);
|
||||||
|
} while (resultSet.next());
|
||||||
|
|
||||||
|
return rewardShareCreators;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch reward share creators from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<TransactionData> getApprovalPendingTransactions(Integer txGroupId, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
public List<TransactionData> getApprovalPendingTransactions(Integer txGroupId, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||||
StringBuilder sql = new StringBuilder(512);
|
StringBuilder sql = new StringBuilder(512);
|
||||||
|
@ -110,7 +110,13 @@ public class Settings {
|
|||||||
/** Maximum number of unconfirmed transactions allowed per account */
|
/** Maximum number of unconfirmed transactions allowed per account */
|
||||||
private int maxUnconfirmedPerAccount = 25;
|
private int maxUnconfirmedPerAccount = 25;
|
||||||
/** Max milliseconds into future for accepting new, unconfirmed transactions */
|
/** Max milliseconds into future for accepting new, unconfirmed transactions */
|
||||||
private int maxTransactionTimestampFuture = 24 * 60 * 60 * 1000; // milliseconds
|
private int maxTransactionTimestampFuture = 30 * 60 * 1000; // milliseconds
|
||||||
|
|
||||||
|
/** Maximum number of CHAT transactions allowed per account in recent timeframe */
|
||||||
|
private int maxRecentChatMessagesPerAccount = 250;
|
||||||
|
/** Maximum age of a CHAT transaction to be considered 'recent' */
|
||||||
|
private long recentChatMessagesMaxAge = 60 * 60 * 1000L; // milliseconds
|
||||||
|
|
||||||
/** Whether we check, fetch and install auto-updates */
|
/** Whether we check, fetch and install auto-updates */
|
||||||
private boolean autoUpdateEnabled = true;
|
private boolean autoUpdateEnabled = true;
|
||||||
/** How long between repository backups (ms), or 0 if disabled. */
|
/** How long between repository backups (ms), or 0 if disabled. */
|
||||||
@ -153,7 +159,7 @@ public class Settings {
|
|||||||
* This prevents the node from being able to serve older blocks */
|
* This prevents the node from being able to serve older blocks */
|
||||||
private boolean topOnly = false;
|
private boolean topOnly = false;
|
||||||
/** The amount of recent blocks we should keep when pruning */
|
/** The amount of recent blocks we should keep when pruning */
|
||||||
private int pruneBlockLimit = 1450;
|
private int pruneBlockLimit = 6000;
|
||||||
|
|
||||||
/** How often to attempt AT state pruning (ms). */
|
/** How often to attempt AT state pruning (ms). */
|
||||||
private long atStatesPruneInterval = 3219L; // milliseconds
|
private long atStatesPruneInterval = 3219L; // milliseconds
|
||||||
@ -209,7 +215,7 @@ public class Settings {
|
|||||||
public long recoveryModeTimeout = 10 * 60 * 1000L;
|
public long recoveryModeTimeout = 10 * 60 * 1000L;
|
||||||
|
|
||||||
/** Minimum peer version number required in order to sync with them */
|
/** Minimum peer version number required in order to sync with them */
|
||||||
private String minPeerVersion = "3.6.3";
|
private String minPeerVersion = "3.8.2";
|
||||||
/** Whether to allow connections with peers below minPeerVersion
|
/** Whether to allow connections with peers below minPeerVersion
|
||||||
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
|
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
|
||||||
* If false, sync will be blocked both ways, and they will not appear in the peers list */
|
* If false, sync will be blocked both ways, and they will not appear in the peers list */
|
||||||
@ -267,7 +273,7 @@ public class Settings {
|
|||||||
private String[] bootstrapHosts = new String[] {
|
private String[] bootstrapHosts = new String[] {
|
||||||
"http://bootstrap.qortal.org",
|
"http://bootstrap.qortal.org",
|
||||||
"http://bootstrap2.qortal.org",
|
"http://bootstrap2.qortal.org",
|
||||||
"http://62.171.190.193"
|
"http://bootstrap.qortal.online"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto-update sources
|
// Auto-update sources
|
||||||
@ -640,6 +646,14 @@ public class Settings {
|
|||||||
return this.maxTransactionTimestampFuture;
|
return this.maxTransactionTimestampFuture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getMaxRecentChatMessagesPerAccount() {
|
||||||
|
return this.maxRecentChatMessagesPerAccount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getRecentChatMessagesMaxAge() {
|
||||||
|
return recentChatMessagesMaxAge;
|
||||||
|
}
|
||||||
|
|
||||||
public int getBlockCacheSize() {
|
public int getBlockCacheSize() {
|
||||||
return this.blockCacheSize;
|
return this.blockCacheSize;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package org.qortal.transaction;
|
|||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
@ -64,15 +65,24 @@ public class AddGroupAdminTransaction extends Transaction {
|
|||||||
|
|
||||||
Account owner = getOwner();
|
Account owner = getOwner();
|
||||||
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
|
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
|
||||||
|
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||||
|
|
||||||
// Check transaction's public key matches group's current owner
|
// Require approval if transaction relates to a group owned by the null account
|
||||||
if (!owner.getAddress().equals(groupOwner))
|
if (groupOwnedByNullAccount && !this.needsGroupApproval())
|
||||||
|
return ValidationResult.GROUP_APPROVAL_REQUIRED;
|
||||||
|
|
||||||
|
// Check transaction's public key matches group's current owner (except for groups owned by the null account)
|
||||||
|
if (!groupOwnedByNullAccount && !owner.getAddress().equals(groupOwner))
|
||||||
return ValidationResult.INVALID_GROUP_OWNER;
|
return ValidationResult.INVALID_GROUP_OWNER;
|
||||||
|
|
||||||
// Check address is a group member
|
// Check address is a group member
|
||||||
if (!this.repository.getGroupRepository().memberExists(groupId, memberAddress))
|
if (!this.repository.getGroupRepository().memberExists(groupId, memberAddress))
|
||||||
return ValidationResult.NOT_GROUP_MEMBER;
|
return ValidationResult.NOT_GROUP_MEMBER;
|
||||||
|
|
||||||
|
// Check transaction creator is a group member
|
||||||
|
if (!this.repository.getGroupRepository().memberExists(groupId, this.getCreator().getAddress()))
|
||||||
|
return ValidationResult.NOT_GROUP_MEMBER;
|
||||||
|
|
||||||
// Check group member is not already an admin
|
// Check group member is not already an admin
|
||||||
if (this.repository.getGroupRepository().adminExists(groupId, memberAddress))
|
if (this.repository.getGroupRepository().adminExists(groupId, memberAddress))
|
||||||
return ValidationResult.ALREADY_GROUP_ADMIN;
|
return ValidationResult.ALREADY_GROUP_ADMIN;
|
||||||
|
@ -24,6 +24,7 @@ import org.qortal.transform.Transformer;
|
|||||||
import org.qortal.transform.transaction.ArbitraryTransactionTransformer;
|
import org.qortal.transform.transaction.ArbitraryTransactionTransformer;
|
||||||
import org.qortal.transform.transaction.TransactionTransformer;
|
import org.qortal.transform.transaction.TransactionTransformer;
|
||||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
public class ArbitraryTransaction extends Transaction {
|
public class ArbitraryTransaction extends Transaction {
|
||||||
|
|
||||||
@ -34,9 +35,13 @@ public class ArbitraryTransaction extends Transaction {
|
|||||||
public static final int MAX_DATA_SIZE = 4000;
|
public static final int MAX_DATA_SIZE = 4000;
|
||||||
public static final int MAX_METADATA_LENGTH = 32;
|
public static final int MAX_METADATA_LENGTH = 32;
|
||||||
public static final int HASH_LENGTH = TransactionTransformer.SHA256_LENGTH;
|
public static final int HASH_LENGTH = TransactionTransformer.SHA256_LENGTH;
|
||||||
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
|
||||||
public static final int MAX_IDENTIFIER_LENGTH = 64;
|
public static final int MAX_IDENTIFIER_LENGTH = 64;
|
||||||
|
|
||||||
|
/** If time difference between transaction and now is greater than this then we don't verify proof-of-work. */
|
||||||
|
public static final long HISTORIC_THRESHOLD = 2 * 7 * 24 * 60 * 60 * 1000L;
|
||||||
|
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||||
|
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
public ArbitraryTransaction(Repository repository, TransactionData transactionData) {
|
public ArbitraryTransaction(Repository repository, TransactionData transactionData) {
|
||||||
@ -202,10 +207,12 @@ public class ArbitraryTransaction extends Transaction {
|
|||||||
// Clear nonce from transactionBytes
|
// Clear nonce from transactionBytes
|
||||||
ArbitraryTransactionTransformer.clearNonce(transactionBytes);
|
ArbitraryTransactionTransformer.clearNonce(transactionBytes);
|
||||||
|
|
||||||
// Check nonce
|
// We only need to check nonce for recent transactions due to PoW verification overhead
|
||||||
|
if (NTP.getTime() - this.arbitraryTransactionData.getTimestamp() < HISTORIC_THRESHOLD) {
|
||||||
int difficulty = ArbitraryDataManager.getInstance().getPowDifficulty();
|
int difficulty = ArbitraryDataManager.getInstance().getPowDifficulty();
|
||||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ public class CancelGroupBanTransaction extends Transaction {
|
|||||||
Account member = getMember();
|
Account member = getMember();
|
||||||
|
|
||||||
// Check ban actually exists
|
// Check ban actually exists
|
||||||
if (!this.repository.getGroupRepository().banExists(groupId, member.getAddress()))
|
if (!this.repository.getGroupRepository().banExists(groupId, member.getAddress(), this.groupUnbanTransactionData.getTimestamp()))
|
||||||
return ValidationResult.BAN_UNKNOWN;
|
return ValidationResult.BAN_UNKNOWN;
|
||||||
|
|
||||||
// Check admin has enough funds
|
// Check admin has enough funds
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package org.qortal.transaction;
|
package org.qortal.transaction;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
import org.qortal.account.PublicKeyAccount;
|
import org.qortal.account.PublicKeyAccount;
|
||||||
@ -16,9 +18,11 @@ import org.qortal.list.ResourceListManager;
|
|||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.GroupRepository;
|
import org.qortal.repository.GroupRepository;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.settings.Settings;
|
||||||
import org.qortal.transform.TransformationException;
|
import org.qortal.transform.TransformationException;
|
||||||
import org.qortal.transform.transaction.ChatTransactionTransformer;
|
import org.qortal.transform.transaction.ChatTransactionTransformer;
|
||||||
import org.qortal.transform.transaction.TransactionTransformer;
|
import org.qortal.transform.transaction.TransactionTransformer;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
public class ChatTransaction extends Transaction {
|
public class ChatTransaction extends Transaction {
|
||||||
|
|
||||||
@ -26,10 +30,11 @@ public class ChatTransaction extends Transaction {
|
|||||||
private ChatTransactionData chatTransactionData;
|
private ChatTransactionData chatTransactionData;
|
||||||
|
|
||||||
// Other useful constants
|
// Other useful constants
|
||||||
public static final int MAX_DATA_SIZE = 1024;
|
public static final int MAX_DATA_SIZE = 4000;
|
||||||
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||||
public static final int POW_DIFFICULTY_WITH_QORT = 8; // leading zero bits
|
public static final int POW_DIFFICULTY_ABOVE_QORT_THRESHOLD = 8; // leading zero bits
|
||||||
public static final int POW_DIFFICULTY_NO_QORT = 12; // leading zero bits
|
public static final int POW_DIFFICULTY_BELOW_QORT_THRESHOLD = 18; // leading zero bits
|
||||||
|
public static final long POW_QORT_THRESHOLD = 400000000L;
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
@ -78,7 +83,7 @@ public class ChatTransaction extends Transaction {
|
|||||||
// Clear nonce from transactionBytes
|
// Clear nonce from transactionBytes
|
||||||
ChatTransactionTransformer.clearNonce(transactionBytes);
|
ChatTransactionTransformer.clearNonce(transactionBytes);
|
||||||
|
|
||||||
int difficulty = this.getSender().getConfirmedBalance(Asset.QORT) > 0 ? POW_DIFFICULTY_WITH_QORT : POW_DIFFICULTY_NO_QORT;
|
int difficulty = this.getSender().getConfirmedBalance(Asset.QORT) >= POW_QORT_THRESHOLD ? POW_DIFFICULTY_ABOVE_QORT_THRESHOLD : POW_DIFFICULTY_BELOW_QORT_THRESHOLD;
|
||||||
|
|
||||||
// Calculate nonce
|
// Calculate nonce
|
||||||
this.chatTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, difficulty));
|
this.chatTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, difficulty));
|
||||||
@ -145,6 +150,11 @@ public class ChatTransaction extends Transaction {
|
|||||||
public ValidationResult isValid() throws DataException {
|
public ValidationResult isValid() throws DataException {
|
||||||
// Nonce checking is done via isSignatureValid() as that method is only called once per import
|
// Nonce checking is done via isSignatureValid() as that method is only called once per import
|
||||||
|
|
||||||
|
// Disregard messages with timestamp too far in the future (we have stricter limits for CHAT transactions)
|
||||||
|
if (this.chatTransactionData.getTimestamp() > NTP.getTime() + (5 * 60 * 1000L)) {
|
||||||
|
return ValidationResult.TIMESTAMP_TOO_NEW;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for blocked author by address
|
// Check for blocked author by address
|
||||||
ResourceListManager listManager = ResourceListManager.getInstance();
|
ResourceListManager listManager = ResourceListManager.getInstance();
|
||||||
if (listManager.listContains("blockedAddresses", this.chatTransactionData.getSender(), true)) {
|
if (listManager.listContains("blockedAddresses", this.chatTransactionData.getSender(), true)) {
|
||||||
@ -163,6 +173,14 @@ public class ChatTransaction extends Transaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PublicKeyAccount creator = this.getCreator();
|
||||||
|
if (creator == null)
|
||||||
|
return ValidationResult.MISSING_CREATOR;
|
||||||
|
|
||||||
|
// Reject if unconfirmed pile already has X recent CHAT transactions from same creator
|
||||||
|
if (countRecentChatTransactionsByCreator(creator) >= Settings.getInstance().getMaxRecentChatMessagesPerAccount())
|
||||||
|
return ValidationResult.TOO_MANY_UNCONFIRMED;
|
||||||
|
|
||||||
// If we exist in the repository then we've been imported as unconfirmed,
|
// If we exist in the repository then we've been imported as unconfirmed,
|
||||||
// but we don't want to make it into a block, so return fake non-OK result.
|
// but we don't want to make it into a block, so return fake non-OK result.
|
||||||
if (this.repository.getTransactionRepository().exists(this.chatTransactionData.getSignature()))
|
if (this.repository.getTransactionRepository().exists(this.chatTransactionData.getSignature()))
|
||||||
@ -204,7 +222,7 @@ public class ChatTransaction extends Transaction {
|
|||||||
|
|
||||||
int difficulty;
|
int difficulty;
|
||||||
try {
|
try {
|
||||||
difficulty = this.getSender().getConfirmedBalance(Asset.QORT) > 0 ? POW_DIFFICULTY_WITH_QORT : POW_DIFFICULTY_NO_QORT;
|
difficulty = this.getSender().getConfirmedBalance(Asset.QORT) >= POW_QORT_THRESHOLD ? POW_DIFFICULTY_ABOVE_QORT_THRESHOLD : POW_DIFFICULTY_BELOW_QORT_THRESHOLD;
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -213,6 +231,26 @@ public class ChatTransaction extends Transaction {
|
|||||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int countRecentChatTransactionsByCreator(PublicKeyAccount creator) throws DataException {
|
||||||
|
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getUnconfirmedTransactions();
|
||||||
|
final Long now = NTP.getTime();
|
||||||
|
long recentThreshold = Settings.getInstance().getRecentChatMessagesMaxAge();
|
||||||
|
|
||||||
|
// We only care about chat transactions, and only those that are considered 'recent'
|
||||||
|
Predicate<TransactionData> hasSameCreatorAndIsRecentChat = transactionData -> {
|
||||||
|
if (transactionData.getType() != TransactionType.CHAT)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (transactionData.getTimestamp() < now - recentThreshold)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Arrays.equals(creator.getPublicKey(), transactionData.getCreatorPublicKey());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (int) unconfirmedTransactions.stream().filter(hasSameCreatorAndIsRecentChat).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure there's at least a skeleton account so people
|
* Ensure there's at least a skeleton account so people
|
||||||
* can retrieve sender's public key using address, even if all their messages
|
* can retrieve sender's public key using address, even if all their messages
|
||||||
|
@ -78,7 +78,7 @@ public class GroupInviteTransaction extends Transaction {
|
|||||||
return ValidationResult.ALREADY_GROUP_MEMBER;
|
return ValidationResult.ALREADY_GROUP_MEMBER;
|
||||||
|
|
||||||
// Check invitee is not banned
|
// Check invitee is not banned
|
||||||
if (this.repository.getGroupRepository().banExists(groupId, invitee.getAddress()))
|
if (this.repository.getGroupRepository().banExists(groupId, invitee.getAddress(), this.groupInviteTransactionData.getTimestamp()))
|
||||||
return ValidationResult.BANNED_FROM_GROUP;
|
return ValidationResult.BANNED_FROM_GROUP;
|
||||||
|
|
||||||
// Check creator has enough funds
|
// Check creator has enough funds
|
||||||
|
@ -53,7 +53,7 @@ public class JoinGroupTransaction extends Transaction {
|
|||||||
return ValidationResult.ALREADY_GROUP_MEMBER;
|
return ValidationResult.ALREADY_GROUP_MEMBER;
|
||||||
|
|
||||||
// Check member is not banned
|
// Check member is not banned
|
||||||
if (this.repository.getGroupRepository().banExists(groupId, joiner.getAddress()))
|
if (this.repository.getGroupRepository().banExists(groupId, joiner.getAddress(), this.joinGroupTransactionData.getTimestamp()))
|
||||||
return ValidationResult.BANNED_FROM_GROUP;
|
return ValidationResult.BANNED_FROM_GROUP;
|
||||||
|
|
||||||
// Check join request doesn't already exist
|
// Check join request doesn't already exist
|
||||||
|
@ -4,7 +4,9 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.account.PublicKeyAccount;
|
||||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||||
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.crypto.MemoryPoW;
|
import org.qortal.crypto.MemoryPoW;
|
||||||
import org.qortal.data.transaction.PublicizeTransactionData;
|
import org.qortal.data.transaction.PublicizeTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
@ -26,7 +28,7 @@ public class PublicizeTransaction extends Transaction {
|
|||||||
/** If time difference between transaction and now is greater than this then we don't verify proof-of-work. */
|
/** If time difference between transaction and now is greater than this then we don't verify proof-of-work. */
|
||||||
public static final long HISTORIC_THRESHOLD = 2 * 7 * 24 * 60 * 60 * 1000L;
|
public static final long HISTORIC_THRESHOLD = 2 * 7 * 24 * 60 * 60 * 1000L;
|
||||||
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||||
public static final int POW_DIFFICULTY = 15; // leading zero bits
|
public static final int POW_DIFFICULTY = 14; // leading zero bits
|
||||||
|
|
||||||
// Constructors
|
// Constructors
|
||||||
|
|
||||||
@ -102,6 +104,12 @@ public class PublicizeTransaction extends Transaction {
|
|||||||
if (!verifyNonce())
|
if (!verifyNonce())
|
||||||
return ValidationResult.INCORRECT_NONCE;
|
return ValidationResult.INCORRECT_NONCE;
|
||||||
|
|
||||||
|
// Validate fee if one has been included
|
||||||
|
PublicKeyAccount creator = this.getCreator();
|
||||||
|
if (this.transactionData.getFee() > 0)
|
||||||
|
if (creator.getConfirmedBalance(Asset.QORT) < this.transactionData.getFee())
|
||||||
|
return ValidationResult.NO_BALANCE;
|
||||||
|
|
||||||
return ValidationResult.OK;
|
return ValidationResult.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package org.qortal.transaction;
|
|||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import org.qortal.account.Account;
|
import org.qortal.account.Account;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
@ -65,11 +66,21 @@ public class RemoveGroupAdminTransaction extends Transaction {
|
|||||||
return ValidationResult.GROUP_DOES_NOT_EXIST;
|
return ValidationResult.GROUP_DOES_NOT_EXIST;
|
||||||
|
|
||||||
Account owner = getOwner();
|
Account owner = getOwner();
|
||||||
|
String groupOwner = this.repository.getGroupRepository().getOwner(groupId);
|
||||||
|
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||||
|
|
||||||
// Check transaction's public key matches group's current owner
|
// Require approval if transaction relates to a group owned by the null account
|
||||||
if (!owner.getAddress().equals(groupData.getOwner()))
|
if (groupOwnedByNullAccount && !this.needsGroupApproval())
|
||||||
|
return ValidationResult.GROUP_APPROVAL_REQUIRED;
|
||||||
|
|
||||||
|
// Check transaction's public key matches group's current owner (except for groups owned by the null account)
|
||||||
|
if (!groupOwnedByNullAccount && !owner.getAddress().equals(groupOwner))
|
||||||
return ValidationResult.INVALID_GROUP_OWNER;
|
return ValidationResult.INVALID_GROUP_OWNER;
|
||||||
|
|
||||||
|
// Check transaction creator is a group member
|
||||||
|
if (!this.repository.getGroupRepository().memberExists(groupId, this.getCreator().getAddress()))
|
||||||
|
return ValidationResult.NOT_GROUP_MEMBER;
|
||||||
|
|
||||||
Account admin = getAdmin();
|
Account admin = getAdmin();
|
||||||
|
|
||||||
// Check member is an admin
|
// Check member is an admin
|
||||||
|
@ -163,9 +163,12 @@ public class RewardShareTransaction extends Transaction {
|
|||||||
return ValidationResult.SELF_SHARE_EXISTS;
|
return ValidationResult.SELF_SHARE_EXISTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fee checking needed if not setting up new self-share
|
|
||||||
if (!(isRecipientAlsoMinter && existingRewardShareData == null))
|
|
||||||
// Check creator has enough funds
|
// Check creator has enough funds
|
||||||
|
if (this.rewardShareTransactionData.getTimestamp() >= BlockChain.getInstance().getFeeValidationFixTimestamp())
|
||||||
|
if (creator.getConfirmedBalance(Asset.QORT) < this.rewardShareTransactionData.getFee())
|
||||||
|
return ValidationResult.NO_BALANCE;
|
||||||
|
|
||||||
|
else if (!(isRecipientAlsoMinter && existingRewardShareData == null))
|
||||||
if (creator.getConfirmedBalance(Asset.QORT) < this.rewardShareTransactionData.getFee())
|
if (creator.getConfirmedBalance(Asset.QORT) < this.rewardShareTransactionData.getFee())
|
||||||
return ValidationResult.NO_BALANCE;
|
return ValidationResult.NO_BALANCE;
|
||||||
|
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
package org.qortal.transaction;
|
package org.qortal.transaction;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.EnumSet;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
@ -69,8 +63,8 @@ public abstract class Transaction {
|
|||||||
AT(21, false),
|
AT(21, false),
|
||||||
CREATE_GROUP(22, true),
|
CREATE_GROUP(22, true),
|
||||||
UPDATE_GROUP(23, true),
|
UPDATE_GROUP(23, true),
|
||||||
ADD_GROUP_ADMIN(24, false),
|
ADD_GROUP_ADMIN(24, true),
|
||||||
REMOVE_GROUP_ADMIN(25, false),
|
REMOVE_GROUP_ADMIN(25, true),
|
||||||
GROUP_BAN(26, false),
|
GROUP_BAN(26, false),
|
||||||
CANCEL_GROUP_BAN(27, false),
|
CANCEL_GROUP_BAN(27, false),
|
||||||
GROUP_KICK(28, false),
|
GROUP_KICK(28, false),
|
||||||
@ -250,6 +244,8 @@ public abstract class Transaction {
|
|||||||
INVALID_TIMESTAMP_SIGNATURE(95),
|
INVALID_TIMESTAMP_SIGNATURE(95),
|
||||||
ADDRESS_BLOCKED(96),
|
ADDRESS_BLOCKED(96),
|
||||||
NAME_BLOCKED(97),
|
NAME_BLOCKED(97),
|
||||||
|
GROUP_APPROVAL_REQUIRED(98),
|
||||||
|
ACCOUNT_NOT_TRANSFERABLE(99),
|
||||||
INVALID_BUT_OK(999),
|
INVALID_BUT_OK(999),
|
||||||
NOT_YET_RELEASED(1000);
|
NOT_YET_RELEASED(1000);
|
||||||
|
|
||||||
@ -760,9 +756,13 @@ public abstract class Transaction {
|
|||||||
// Group no longer exists? Possibly due to blockchain orphaning undoing group creation?
|
// Group no longer exists? Possibly due to blockchain orphaning undoing group creation?
|
||||||
return true; // stops tx being included in block but it will eventually expire
|
return true; // stops tx being included in block but it will eventually expire
|
||||||
|
|
||||||
|
String groupOwner = this.repository.getGroupRepository().getOwner(txGroupId);
|
||||||
|
boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS);
|
||||||
|
|
||||||
// If transaction's creator is group admin (of group with ID txGroupId) then auto-approve
|
// If transaction's creator is group admin (of group with ID txGroupId) then auto-approve
|
||||||
|
// This is disabled for null-owned groups, since these require approval from other admins
|
||||||
PublicKeyAccount creator = this.getCreator();
|
PublicKeyAccount creator = this.getCreator();
|
||||||
if (groupRepository.adminExists(txGroupId, creator.getAddress()))
|
if (!groupOwnedByNullAccount && groupRepository.adminExists(txGroupId, creator.getAddress()))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -67,6 +67,11 @@ public class TransferPrivsTransaction extends Transaction {
|
|||||||
if (getSender().getConfirmedBalance(Asset.QORT) < this.transferPrivsTransactionData.getFee())
|
if (getSender().getConfirmedBalance(Asset.QORT) < this.transferPrivsTransactionData.getFee())
|
||||||
return ValidationResult.NO_BALANCE;
|
return ValidationResult.NO_BALANCE;
|
||||||
|
|
||||||
|
// Check sender doesn't have a blocksMintedPenalty, as these accounts cannot be transferred
|
||||||
|
AccountData senderAccountData = this.repository.getAccountRepository().getAccount(getSender().getAddress());
|
||||||
|
if (senderAccountData == null || senderAccountData.getBlocksMintedPenalty() != 0)
|
||||||
|
return ValidationResult.ACCOUNT_NOT_TRANSFERABLE;
|
||||||
|
|
||||||
return ValidationResult.OK;
|
return ValidationResult.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ public class UpdateGroupTransaction extends Transaction {
|
|||||||
Account newOwner = getNewOwner();
|
Account newOwner = getNewOwner();
|
||||||
|
|
||||||
// Check new owner is not banned
|
// Check new owner is not banned
|
||||||
if (this.repository.getGroupRepository().banExists(this.updateGroupTransactionData.getGroupId(), newOwner.getAddress()))
|
if (this.repository.getGroupRepository().banExists(this.updateGroupTransactionData.getGroupId(), newOwner.getAddress(), this.updateGroupTransactionData.getTimestamp()))
|
||||||
return ValidationResult.BANNED_FROM_GROUP;
|
return ValidationResult.BANNED_FROM_GROUP;
|
||||||
|
|
||||||
return ValidationResult.OK;
|
return ValidationResult.OK;
|
||||||
|
@ -4,6 +4,7 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.transaction.BaseTransactionData;
|
import org.qortal.data.transaction.BaseTransactionData;
|
||||||
import org.qortal.data.transaction.ChatTransactionData;
|
import org.qortal.data.transaction.ChatTransactionData;
|
||||||
@ -22,11 +23,13 @@ public class ChatTransactionTransformer extends TransactionTransformer {
|
|||||||
private static final int NONCE_LENGTH = INT_LENGTH;
|
private static final int NONCE_LENGTH = INT_LENGTH;
|
||||||
private static final int HAS_RECIPIENT_LENGTH = BOOLEAN_LENGTH;
|
private static final int HAS_RECIPIENT_LENGTH = BOOLEAN_LENGTH;
|
||||||
private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH;
|
private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH;
|
||||||
|
private static final int HAS_CHAT_REFERENCE_LENGTH = BOOLEAN_LENGTH;
|
||||||
|
private static final int CHAT_REFERENCE_LENGTH = SIGNATURE_LENGTH;
|
||||||
private static final int DATA_SIZE_LENGTH = INT_LENGTH;
|
private static final int DATA_SIZE_LENGTH = INT_LENGTH;
|
||||||
private static final int IS_TEXT_LENGTH = BOOLEAN_LENGTH;
|
private static final int IS_TEXT_LENGTH = BOOLEAN_LENGTH;
|
||||||
private static final int IS_ENCRYPTED_LENGTH = BOOLEAN_LENGTH;
|
private static final int IS_ENCRYPTED_LENGTH = BOOLEAN_LENGTH;
|
||||||
|
|
||||||
private static final int EXTRAS_LENGTH = NONCE_LENGTH + HAS_RECIPIENT_LENGTH + DATA_SIZE_LENGTH + IS_ENCRYPTED_LENGTH + IS_TEXT_LENGTH;
|
private static final int EXTRAS_LENGTH = NONCE_LENGTH + HAS_RECIPIENT_LENGTH + DATA_SIZE_LENGTH + IS_ENCRYPTED_LENGTH + IS_TEXT_LENGTH + HAS_CHAT_REFERENCE_LENGTH;
|
||||||
|
|
||||||
protected static final TransactionLayout layout;
|
protected static final TransactionLayout layout;
|
||||||
|
|
||||||
@ -77,13 +80,24 @@ public class ChatTransactionTransformer extends TransactionTransformer {
|
|||||||
|
|
||||||
long fee = byteBuffer.getLong();
|
long fee = byteBuffer.getLong();
|
||||||
|
|
||||||
|
byte[] chatReference = null;
|
||||||
|
|
||||||
|
if (timestamp >= BlockChain.getInstance().getChatReferenceTimestamp()) {
|
||||||
|
boolean hasChatReference = byteBuffer.get() != 0;
|
||||||
|
|
||||||
|
if (hasChatReference) {
|
||||||
|
chatReference = new byte[CHAT_REFERENCE_LENGTH];
|
||||||
|
byteBuffer.get(chatReference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||||
byteBuffer.get(signature);
|
byteBuffer.get(signature);
|
||||||
|
|
||||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderPublicKey, fee, signature);
|
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderPublicKey, fee, signature);
|
||||||
|
|
||||||
String sender = Crypto.toAddress(senderPublicKey);
|
String sender = Crypto.toAddress(senderPublicKey);
|
||||||
return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, data, isText, isEncrypted);
|
return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, chatReference, data, isText, isEncrypted);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getDataLength(TransactionData transactionData) {
|
public static int getDataLength(TransactionData transactionData) {
|
||||||
@ -94,6 +108,9 @@ public class ChatTransactionTransformer extends TransactionTransformer {
|
|||||||
if (chatTransactionData.getRecipient() != null)
|
if (chatTransactionData.getRecipient() != null)
|
||||||
dataLength += RECIPIENT_LENGTH;
|
dataLength += RECIPIENT_LENGTH;
|
||||||
|
|
||||||
|
if (chatTransactionData.getChatReference() != null)
|
||||||
|
dataLength += CHAT_REFERENCE_LENGTH;
|
||||||
|
|
||||||
return dataLength;
|
return dataLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,6 +141,16 @@ public class ChatTransactionTransformer extends TransactionTransformer {
|
|||||||
|
|
||||||
bytes.write(Longs.toByteArray(chatTransactionData.getFee()));
|
bytes.write(Longs.toByteArray(chatTransactionData.getFee()));
|
||||||
|
|
||||||
|
if (transactionData.getTimestamp() >= BlockChain.getInstance().getChatReferenceTimestamp()) {
|
||||||
|
// Include chat reference if it's not null
|
||||||
|
if (chatTransactionData.getChatReference() != null) {
|
||||||
|
bytes.write((byte) 1);
|
||||||
|
bytes.write(chatTransactionData.getChatReference());
|
||||||
|
} else {
|
||||||
|
bytes.write((byte) 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (chatTransactionData.getSignature() != null)
|
if (chatTransactionData.getSignature() != null)
|
||||||
bytes.write(chatTransactionData.getSignature());
|
bytes.write(chatTransactionData.getSignature());
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
"onlineAccountSignaturesMinLifetime": 43200000,
|
"onlineAccountSignaturesMinLifetime": 43200000,
|
||||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
"onlineAccountsModulusV2Timestamp": 1659801600000,
|
"onlineAccountsModulusV2Timestamp": 1659801600000,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 1670230000000,
|
||||||
"rewardsByHeight": [
|
"rewardsByHeight": [
|
||||||
{ "height": 1, "reward": 5.00 },
|
{ "height": 1, "reward": 5.00 },
|
||||||
{ "height": 259201, "reward": 4.75 },
|
{ "height": 259201, "reward": 4.75 },
|
||||||
@ -80,8 +81,15 @@
|
|||||||
"transactionV5Timestamp": 1642176000000,
|
"transactionV5Timestamp": 1642176000000,
|
||||||
"transactionV6Timestamp": 9999999999999,
|
"transactionV6Timestamp": 9999999999999,
|
||||||
"disableReferenceTimestamp": 1655222400000,
|
"disableReferenceTimestamp": 1655222400000,
|
||||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 1092000,
|
||||||
|
"selfSponsorshipAlgoV1Height": 1092400,
|
||||||
|
"feeValidationFixTimestamp": 1671918000000,
|
||||||
|
"chatReferenceTimestamp": 1674316800000
|
||||||
},
|
},
|
||||||
|
"checkpoints": [
|
||||||
|
{ "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" }
|
||||||
|
],
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"timestamp": "1593450000000",
|
"timestamp": "1593450000000",
|
||||||
|
1572
src/test/java/org/qortal/test/SelfSponsorshipAlgoV1Tests.java
Normal file
1572
src/test/java/org/qortal/test/SelfSponsorshipAlgoV1Tests.java
Normal file
File diff suppressed because it is too large
Load Diff
@ -84,7 +84,7 @@ public class BlockApiTests extends ApiCommon {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetBlockRange() {
|
public void testGetBlockRange() {
|
||||||
assertNotNull(this.blocksResource.getBlockRange(1, 1));
|
assertNotNull(this.blocksResource.getBlockRange(1, 1, false, false));
|
||||||
|
|
||||||
List<Integer> testValues = Arrays.asList(null, Integer.valueOf(1));
|
List<Integer> testValues = Arrays.asList(null, Integer.valueOf(1));
|
||||||
|
|
||||||
|
@ -1,11 +1,26 @@
|
|||||||
package org.qortal.test.arbitrary;
|
package org.qortal.test.arbitrary;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
|
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||||
|
import org.qortal.arbitrary.ArbitraryDataReader;
|
||||||
|
import org.qortal.arbitrary.exception.MissingDataException;
|
||||||
import org.qortal.arbitrary.misc.Service;
|
import org.qortal.arbitrary.misc.Service;
|
||||||
import org.qortal.arbitrary.misc.Service.ValidationResult;
|
import org.qortal.arbitrary.misc.Service.ValidationResult;
|
||||||
|
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||||
|
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||||
|
import org.qortal.data.transaction.RegisterNameTransactionData;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
import org.qortal.test.common.ArbitraryUtils;
|
||||||
import org.qortal.test.common.Common;
|
import org.qortal.test.common.Common;
|
||||||
|
import org.qortal.test.common.TransactionUtils;
|
||||||
|
import org.qortal.test.common.transaction.TestTransaction;
|
||||||
|
import org.qortal.transaction.RegisterNameTransaction;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -117,10 +132,27 @@ public class ArbitraryServiceTests extends Common {
|
|||||||
Service service = Service.GIF_REPOSITORY;
|
Service service = Service.GIF_REPOSITORY;
|
||||||
assertTrue(service.isValidationRequired());
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
// There is an index file in the root
|
|
||||||
assertEquals(ValidationResult.OK, service.validate(path));
|
assertEquals(ValidationResult.OK, service.validate(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidateSingleFileGifRepository() throws IOException {
|
||||||
|
// Generate some random data
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
new Random().nextBytes(data);
|
||||||
|
|
||||||
|
// Write the data to a single file in a temp path
|
||||||
|
Path path = Files.createTempDirectory("testValidateSingleFileGifRepository");
|
||||||
|
path.toFile().deleteOnExit();
|
||||||
|
Path imagePath = Paths.get(path.toString(), "image1.gif");
|
||||||
|
Files.write(imagePath, data, StandardOpenOption.CREATE);
|
||||||
|
|
||||||
|
Service service = Service.GIF_REPOSITORY;
|
||||||
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
|
assertEquals(ValidationResult.OK, service.validate(imagePath));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testValidateMultiLayerGifRepository() throws IOException {
|
public void testValidateMultiLayerGifRepository() throws IOException {
|
||||||
// Generate some random data
|
// Generate some random data
|
||||||
@ -140,7 +172,6 @@ public class ArbitraryServiceTests extends Common {
|
|||||||
Service service = Service.GIF_REPOSITORY;
|
Service service = Service.GIF_REPOSITORY;
|
||||||
assertTrue(service.isValidationRequired());
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
// There is an index file in the root
|
|
||||||
assertEquals(ValidationResult.DIRECTORIES_NOT_ALLOWED, service.validate(path));
|
assertEquals(ValidationResult.DIRECTORIES_NOT_ALLOWED, service.validate(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,7 +182,6 @@ public class ArbitraryServiceTests extends Common {
|
|||||||
Service service = Service.GIF_REPOSITORY;
|
Service service = Service.GIF_REPOSITORY;
|
||||||
assertTrue(service.isValidationRequired());
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
// There is an index file in the root
|
|
||||||
assertEquals(ValidationResult.MISSING_DATA, service.validate(path));
|
assertEquals(ValidationResult.MISSING_DATA, service.validate(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,8 +201,192 @@ public class ArbitraryServiceTests extends Common {
|
|||||||
Service service = Service.GIF_REPOSITORY;
|
Service service = Service.GIF_REPOSITORY;
|
||||||
assertTrue(service.isValidationRequired());
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
// There is an index file in the root
|
|
||||||
assertEquals(ValidationResult.INVALID_FILE_EXTENSION, service.validate(path));
|
assertEquals(ValidationResult.INVALID_FILE_EXTENSION, service.validate(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidatePublishedGifRepository() throws IOException, DataException, MissingDataException, IllegalAccessException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
// Generate some random data
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
new Random().nextBytes(data);
|
||||||
|
|
||||||
|
// Write the data to several files in a temp path
|
||||||
|
Path path = Files.createTempDirectory("testValidateGifRepository");
|
||||||
|
path.toFile().deleteOnExit();
|
||||||
|
Files.write(Paths.get(path.toString(), "image1.gif"), data, StandardOpenOption.CREATE);
|
||||||
|
Files.write(Paths.get(path.toString(), "image2.gif"), data, StandardOpenOption.CREATE);
|
||||||
|
Files.write(Paths.get(path.toString(), "image3.gif"), data, StandardOpenOption.CREATE);
|
||||||
|
|
||||||
|
Service service = Service.GIF_REPOSITORY;
|
||||||
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
|
assertEquals(ValidationResult.OK, service.validate(path));
|
||||||
|
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||||
|
String name = "TEST"; // Can be anything for this test
|
||||||
|
String identifier = "test_identifier";
|
||||||
|
|
||||||
|
// Register the name to Alice
|
||||||
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
|
// Set difficulty to 1
|
||||||
|
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||||
|
|
||||||
|
// Create PUT transaction
|
||||||
|
ArbitraryUtils.createAndMintTxn(repository, publicKey58, path, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice);
|
||||||
|
|
||||||
|
// Build the latest data state for this name, and no exceptions should be thrown because validation passes
|
||||||
|
ArbitraryDataReader arbitraryDataReader1a = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||||
|
arbitraryDataReader1a.loadSynchronously(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidateQChatAttachment() throws IOException {
|
||||||
|
// Generate some random data
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
new Random().nextBytes(data);
|
||||||
|
|
||||||
|
// Write the data a single file in a temp path
|
||||||
|
Path path = Files.createTempDirectory("testValidateQChatAttachment");
|
||||||
|
path.toFile().deleteOnExit();
|
||||||
|
Files.write(Paths.get(path.toString(), "document.pdf"), data, StandardOpenOption.CREATE);
|
||||||
|
|
||||||
|
Service service = Service.QCHAT_ATTACHMENT;
|
||||||
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
|
assertEquals(ValidationResult.OK, service.validate(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidateSingleFileQChatAttachment() throws IOException {
|
||||||
|
// Generate some random data
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
new Random().nextBytes(data);
|
||||||
|
|
||||||
|
// Write the data a single file in a temp path
|
||||||
|
Path path = Files.createTempDirectory("testValidateSingleFileQChatAttachment");
|
||||||
|
path.toFile().deleteOnExit();
|
||||||
|
Path filePath = Paths.get(path.toString(), "document.pdf");
|
||||||
|
Files.write(filePath, data, StandardOpenOption.CREATE);
|
||||||
|
|
||||||
|
Service service = Service.QCHAT_ATTACHMENT;
|
||||||
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
|
assertEquals(ValidationResult.OK, service.validate(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidateInvalidQChatAttachmentFileExtension() throws IOException {
|
||||||
|
// Generate some random data
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
new Random().nextBytes(data);
|
||||||
|
|
||||||
|
// Write the data a single file in a temp path
|
||||||
|
Path path = Files.createTempDirectory("testValidateInvalidQChatAttachmentFileExtension");
|
||||||
|
path.toFile().deleteOnExit();
|
||||||
|
Files.write(Paths.get(path.toString(), "application.exe"), data, StandardOpenOption.CREATE);
|
||||||
|
|
||||||
|
Service service = Service.QCHAT_ATTACHMENT;
|
||||||
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
|
assertEquals(ValidationResult.INVALID_FILE_EXTENSION, service.validate(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidateEmptyQChatAttachment() throws IOException {
|
||||||
|
Path path = Files.createTempDirectory("testValidateEmptyQChatAttachment");
|
||||||
|
|
||||||
|
Service service = Service.QCHAT_ATTACHMENT;
|
||||||
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
|
assertEquals(ValidationResult.INVALID_FILE_COUNT, service.validate(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidateMultiLayerQChatAttachment() throws IOException {
|
||||||
|
// Generate some random data
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
new Random().nextBytes(data);
|
||||||
|
|
||||||
|
// Write the data to several files in a temp path
|
||||||
|
Path path = Files.createTempDirectory("testValidateMultiLayerQChatAttachment");
|
||||||
|
path.toFile().deleteOnExit();
|
||||||
|
Files.write(Paths.get(path.toString(), "file1.txt"), data, StandardOpenOption.CREATE);
|
||||||
|
|
||||||
|
Path subdirectory = Paths.get(path.toString(), "subdirectory");
|
||||||
|
Files.createDirectories(subdirectory);
|
||||||
|
Files.write(Paths.get(subdirectory.toString(), "file2.txt"), data, StandardOpenOption.CREATE);
|
||||||
|
Files.write(Paths.get(subdirectory.toString(), "file3.txt"), data, StandardOpenOption.CREATE);
|
||||||
|
|
||||||
|
Service service = Service.QCHAT_ATTACHMENT;
|
||||||
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
|
assertEquals(ValidationResult.DIRECTORIES_NOT_ALLOWED, service.validate(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidateMultiFileQChatAttachment() throws IOException {
|
||||||
|
// Generate some random data
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
new Random().nextBytes(data);
|
||||||
|
|
||||||
|
// Write the data to several files in a temp path
|
||||||
|
Path path = Files.createTempDirectory("testValidateMultiFileQChatAttachment");
|
||||||
|
path.toFile().deleteOnExit();
|
||||||
|
Files.write(Paths.get(path.toString(), "file1.txt"), data, StandardOpenOption.CREATE);
|
||||||
|
Files.write(Paths.get(path.toString(), "file2.txt"), data, StandardOpenOption.CREATE);
|
||||||
|
|
||||||
|
Service service = Service.QCHAT_ATTACHMENT;
|
||||||
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
|
assertEquals(ValidationResult.INVALID_FILE_COUNT, service.validate(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidatePublishedQChatAttachment() throws IOException, DataException, MissingDataException, IllegalAccessException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
// Generate some random data
|
||||||
|
byte[] data = new byte[1024];
|
||||||
|
new Random().nextBytes(data);
|
||||||
|
|
||||||
|
// Write the data a single file in a temp path
|
||||||
|
Path path = Files.createTempDirectory("testValidateSingleFileQChatAttachment");
|
||||||
|
path.toFile().deleteOnExit();
|
||||||
|
Path filePath = Paths.get(path.toString(), "document.pdf");
|
||||||
|
Files.write(filePath, data, StandardOpenOption.CREATE);
|
||||||
|
|
||||||
|
Service service = Service.QCHAT_ATTACHMENT;
|
||||||
|
assertTrue(service.isValidationRequired());
|
||||||
|
|
||||||
|
assertEquals(ValidationResult.OK, service.validate(filePath));
|
||||||
|
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
String publicKey58 = Base58.encode(alice.getPublicKey());
|
||||||
|
String name = "TEST"; // Can be anything for this test
|
||||||
|
String identifier = "test_identifier";
|
||||||
|
|
||||||
|
// Register the name to Alice
|
||||||
|
RegisterNameTransactionData transactionData = new RegisterNameTransactionData(TestTransaction.generateBase(alice), name, "");
|
||||||
|
transactionData.setFee(new RegisterNameTransaction(null, null).getUnitFee(transactionData.getTimestamp()));
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, alice);
|
||||||
|
|
||||||
|
// Set difficulty to 1
|
||||||
|
FieldUtils.writeField(ArbitraryDataManager.getInstance(), "powDifficulty", 1, true);
|
||||||
|
|
||||||
|
// Create PUT transaction
|
||||||
|
ArbitraryUtils.createAndMintTxn(repository, publicKey58, filePath, name, identifier, ArbitraryTransactionData.Method.PUT, service, alice);
|
||||||
|
|
||||||
|
// Build the latest data state for this name, and no exceptions should be thrown because validation passes
|
||||||
|
ArbitraryDataReader arbitraryDataReader1a = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||||
|
arbitraryDataReader1a.loadSynchronously(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -8,7 +8,6 @@ import java.util.*;
|
|||||||
|
|
||||||
import com.google.common.primitives.Longs;
|
import com.google.common.primitives.Longs;
|
||||||
import org.qortal.account.PrivateKeyAccount;
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
import org.qortal.block.BlockChain;
|
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.crypto.Qortal25519Extras;
|
import org.qortal.crypto.Qortal25519Extras;
|
||||||
import org.qortal.data.network.OnlineAccountData;
|
import org.qortal.data.network.OnlineAccountData;
|
||||||
@ -19,6 +18,7 @@ import org.qortal.data.transaction.TransactionData;
|
|||||||
import org.qortal.group.Group;
|
import org.qortal.group.Group;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.transaction.Transaction;
|
||||||
import org.qortal.transform.Transformer;
|
import org.qortal.transform.Transformer;
|
||||||
import org.qortal.utils.Amounts;
|
import org.qortal.utils.Amounts;
|
||||||
|
|
||||||
@ -49,10 +49,10 @@ public class AccountUtils {
|
|||||||
public static TransactionData createRewardShare(Repository repository, String minter, String recipient, int sharePercent) throws DataException {
|
public static TransactionData createRewardShare(Repository repository, String minter, String recipient, int sharePercent) throws DataException {
|
||||||
PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, minter);
|
PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, minter);
|
||||||
PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, recipient);
|
PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, recipient);
|
||||||
return createRewardShare(repository, mintingAccount, recipientAccount, sharePercent);
|
return createRewardShare(repository, mintingAccount, recipientAccount, sharePercent, fee);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TransactionData createRewardShare(Repository repository, PrivateKeyAccount mintingAccount, PrivateKeyAccount recipientAccount, int sharePercent) throws DataException {
|
public static TransactionData createRewardShare(Repository repository, PrivateKeyAccount mintingAccount, PrivateKeyAccount recipientAccount, int sharePercent, long fee) throws DataException {
|
||||||
byte[] reference = mintingAccount.getLastReference();
|
byte[] reference = mintingAccount.getLastReference();
|
||||||
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
|
long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ public class AccountUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] rewardShare(Repository repository, PrivateKeyAccount minterAccount, PrivateKeyAccount recipientAccount, int sharePercent) throws DataException {
|
public static byte[] rewardShare(Repository repository, PrivateKeyAccount minterAccount, PrivateKeyAccount recipientAccount, int sharePercent) throws DataException {
|
||||||
TransactionData transactionData = createRewardShare(repository, minterAccount, recipientAccount, sharePercent);
|
TransactionData transactionData = createRewardShare(repository, minterAccount, recipientAccount, sharePercent, fee);
|
||||||
|
|
||||||
TransactionUtils.signAndMint(repository, transactionData, minterAccount);
|
TransactionUtils.signAndMint(repository, transactionData, minterAccount);
|
||||||
byte[] rewardSharePrivateKey = minterAccount.getRewardSharePrivateKey(recipientAccount.getPublicKey());
|
byte[] rewardSharePrivateKey = minterAccount.getRewardSharePrivateKey(recipientAccount.getPublicKey());
|
||||||
@ -86,6 +86,61 @@ public class AccountUtils {
|
|||||||
return rewardSharePrivateKey;
|
return rewardSharePrivateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<PrivateKeyAccount> generateSponsorshipRewardShares(Repository repository, PrivateKeyAccount sponsorAccount, int accountsCount) throws DataException {
|
||||||
|
final int sharePercent = 0;
|
||||||
|
Random random = new Random();
|
||||||
|
|
||||||
|
List<PrivateKeyAccount> sponsees = new ArrayList<>();
|
||||||
|
for (int i = 0; i < accountsCount; i++) {
|
||||||
|
|
||||||
|
// Generate random sponsee account
|
||||||
|
byte[] randomPrivateKey = new byte[32];
|
||||||
|
random.nextBytes(randomPrivateKey);
|
||||||
|
PrivateKeyAccount sponseeAccount = new PrivateKeyAccount(repository, randomPrivateKey);
|
||||||
|
sponsees.add(sponseeAccount);
|
||||||
|
|
||||||
|
// Create reward-share
|
||||||
|
TransactionData transactionData = AccountUtils.createRewardShare(repository, sponsorAccount, sponseeAccount, sharePercent, fee);
|
||||||
|
TransactionUtils.signAndImportValid(repository, transactionData, sponsorAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sponsees;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Transaction.ValidationResult createRandomRewardShare(Repository repository, PrivateKeyAccount account) throws DataException {
|
||||||
|
// Bob attempts to create a reward share transaction
|
||||||
|
byte[] randomPrivateKey = new byte[32];
|
||||||
|
new Random().nextBytes(randomPrivateKey);
|
||||||
|
PrivateKeyAccount sponseeAccount = new PrivateKeyAccount(repository, randomPrivateKey);
|
||||||
|
TransactionData transactionData = createRewardShare(repository, account, sponseeAccount, 0, fee);
|
||||||
|
return TransactionUtils.signAndImport(repository, transactionData, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PrivateKeyAccount> generateSelfShares(Repository repository, List<PrivateKeyAccount> accounts) throws DataException {
|
||||||
|
final int sharePercent = 0;
|
||||||
|
|
||||||
|
for (PrivateKeyAccount account : accounts) {
|
||||||
|
// Create reward-share
|
||||||
|
TransactionData transactionData = createRewardShare(repository, account, account, sharePercent, 0L);
|
||||||
|
TransactionUtils.signAndImportValid(repository, transactionData, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toRewardShares(repository, null, accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PrivateKeyAccount> toRewardShares(Repository repository, PrivateKeyAccount parentAccount, List<PrivateKeyAccount> accounts) {
|
||||||
|
List<PrivateKeyAccount> rewardShares = new ArrayList<>();
|
||||||
|
|
||||||
|
for (PrivateKeyAccount account : accounts) {
|
||||||
|
PrivateKeyAccount sponsor = (parentAccount != null) ? parentAccount : account;
|
||||||
|
byte[] rewardSharePrivateKey = sponsor.getRewardSharePrivateKey(account.getPublicKey());
|
||||||
|
PrivateKeyAccount rewardShareAccount = new PrivateKeyAccount(repository, rewardSharePrivateKey);
|
||||||
|
rewardShares.add(rewardShareAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rewardShares;
|
||||||
|
}
|
||||||
|
|
||||||
public static Map<String, Map<Long, Long>> getBalances(Repository repository, long... assetIds) throws DataException {
|
public static Map<String, Map<Long, Long>> getBalances(Repository repository, long... assetIds) throws DataException {
|
||||||
Map<String, Map<Long, Long>> balances = new HashMap<>();
|
Map<String, Map<Long, Long>> balances = new HashMap<>();
|
||||||
|
|
||||||
|
@ -120,7 +120,9 @@ public class Common {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void useSettingsAndDb(String settingsFilename, boolean dbInMemory) throws DataException {
|
public static void useSettingsAndDb(String settingsFilename, boolean dbInMemory) throws DataException {
|
||||||
|
if (RepositoryManager.getRepositoryFactory() != null) {
|
||||||
closeRepository();
|
closeRepository();
|
||||||
|
}
|
||||||
|
|
||||||
// Load/check settings, which potentially sets up blockchain config, etc.
|
// Load/check settings, which potentially sets up blockchain config, etc.
|
||||||
LOGGER.debug(String.format("Using setting file: %s", settingsFilename));
|
LOGGER.debug(String.format("Using setting file: %s", settingsFilename));
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
package org.qortal.test.common.transaction;
|
||||||
|
|
||||||
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.data.transaction.ChatTransactionData;
|
||||||
|
import org.qortal.data.transaction.TransactionData;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
public class ChatTestTransaction extends TestTransaction {
|
||||||
|
|
||||||
|
public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException {
|
||||||
|
Random random = new Random();
|
||||||
|
byte[] orderId = new byte[64];
|
||||||
|
random.nextBytes(orderId);
|
||||||
|
|
||||||
|
String sender = Crypto.toAddress(account.getPublicKey());
|
||||||
|
int nonce = 1234567;
|
||||||
|
|
||||||
|
// Generate random recipient
|
||||||
|
byte[] randomPrivateKey = new byte[32];
|
||||||
|
random.nextBytes(randomPrivateKey);
|
||||||
|
PrivateKeyAccount recipientAccount = new PrivateKeyAccount(repository, randomPrivateKey);
|
||||||
|
String recipient = Crypto.toAddress(recipientAccount.getPublicKey());
|
||||||
|
|
||||||
|
byte[] chatReference = new byte[64];
|
||||||
|
random.nextBytes(chatReference);
|
||||||
|
|
||||||
|
byte[] data = new byte[4000];
|
||||||
|
random.nextBytes(data);
|
||||||
|
|
||||||
|
boolean isText = true;
|
||||||
|
boolean isEncrypted = true;
|
||||||
|
|
||||||
|
return new ChatTransactionData(generateBase(account), sender, nonce, recipient, chatReference, data, isText, isEncrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -135,7 +135,8 @@ public class AdminTests extends Common {
|
|||||||
assertNotSame(ValidationResult.OK, result);
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
// Attempt to ban Bob
|
// Attempt to ban Bob
|
||||||
result = groupBan(repository, alice, groupId, bob.getAddress());
|
int timeToLive = 0;
|
||||||
|
result = groupBan(repository, alice, groupId, bob.getAddress(), timeToLive);
|
||||||
// Should be OK
|
// Should be OK
|
||||||
assertEquals(ValidationResult.OK, result);
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
@ -158,7 +159,7 @@ public class AdminTests extends Common {
|
|||||||
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
// Attempt to ban Bob
|
// Attempt to ban Bob
|
||||||
result = groupBan(repository, alice, groupId, bob.getAddress());
|
result = groupBan(repository, alice, groupId, bob.getAddress(), timeToLive);
|
||||||
// Should be OK
|
// Should be OK
|
||||||
assertEquals(ValidationResult.OK, result);
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
@ -205,6 +206,144 @@ public class AdminTests extends Common {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupBanMemberWithExpiry() throws DataException, InterruptedException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||||
|
|
||||||
|
// Create group
|
||||||
|
int groupId = createGroup(repository, alice, "open-group", true);
|
||||||
|
|
||||||
|
// Confirm Bob is not a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Attempt to cancel non-existent Bob ban
|
||||||
|
ValidationResult result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Attempt to ban Bob for 2 seconds
|
||||||
|
int timeToLive = 2;
|
||||||
|
result = groupBan(repository, alice, groupId, bob.getAddress(), timeToLive);
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Confirm Bob no longer a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Bob attempts to rejoin
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Wait for 2 seconds to pass
|
||||||
|
Thread.sleep(2000L);
|
||||||
|
|
||||||
|
// Bob attempts to rejoin again
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should be OK, as the ban has expired
|
||||||
|
assertSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Confirm Bob is now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Orphan last block (Bob join)
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
|
||||||
|
// Confirm Bob is not a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Orphan last block (Bob ban)
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
// Delete unconfirmed group-ban transaction
|
||||||
|
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||||
|
|
||||||
|
// Bob to join
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Confirm Bob now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
|
||||||
|
// Attempt to ban Bob for 2 seconds
|
||||||
|
result = groupBan(repository, alice, groupId, bob.getAddress(), 2);
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Confirm Bob no longer a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Wait for 2 seconds to pass
|
||||||
|
Thread.sleep(2000L);
|
||||||
|
|
||||||
|
// Cancel Bob's ban
|
||||||
|
result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should NOT be OK, as ban has already expired
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Confirm Bob still not a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Bob attempts to rejoin
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should be OK, as no longer banned
|
||||||
|
assertSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Confirm Bob is now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
|
||||||
|
// Attempt to ban Bob for 10 seconds
|
||||||
|
result = groupBan(repository, alice, groupId, bob.getAddress(), 10);
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Confirm Bob no longer a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Bob attempts to rejoin
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should NOT be OK, as ban still exists
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Cancel Bob's ban
|
||||||
|
result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should be OK, as ban still exists
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Bob attempts to rejoin
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should be OK, as no longer banned
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Orphan last block (Bob join)
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
// Delete unconfirmed join-group transaction
|
||||||
|
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||||
|
|
||||||
|
// Orphan last block (Cancel Bob ban)
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
// Delete unconfirmed cancel-ban transaction
|
||||||
|
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||||
|
|
||||||
|
// Bob attempts to rejoin
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Orphan last block (Bob ban)
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
// Delete unconfirmed group-ban transaction
|
||||||
|
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||||
|
|
||||||
|
// Confirm Bob now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGroupBanAdmin() throws DataException {
|
public void testGroupBanAdmin() throws DataException {
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
@ -226,7 +365,8 @@ public class AdminTests extends Common {
|
|||||||
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
|
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
// Attempt to ban Bob
|
// Attempt to ban Bob
|
||||||
result = groupBan(repository, alice, groupId, bob.getAddress());
|
int timeToLive = 0;
|
||||||
|
result = groupBan(repository, alice, groupId, bob.getAddress(), timeToLive);
|
||||||
// Should be OK
|
// Should be OK
|
||||||
assertEquals(ValidationResult.OK, result);
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
@ -272,12 +412,12 @@ public class AdminTests extends Common {
|
|||||||
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
|
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
// Have Alice (owner) try to ban herself!
|
// Have Alice (owner) try to ban herself!
|
||||||
result = groupBan(repository, alice, groupId, alice.getAddress());
|
result = groupBan(repository, alice, groupId, alice.getAddress(), timeToLive);
|
||||||
// Should NOT be OK
|
// Should NOT be OK
|
||||||
assertNotSame(ValidationResult.OK, result);
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
// Have Bob try to ban Alice (owner)
|
// Have Bob try to ban Alice (owner)
|
||||||
result = groupBan(repository, bob, groupId, alice.getAddress());
|
result = groupBan(repository, bob, groupId, alice.getAddress(), timeToLive);
|
||||||
// Should NOT be OK
|
// Should NOT be OK
|
||||||
assertNotSame(ValidationResult.OK, result);
|
assertNotSame(ValidationResult.OK, result);
|
||||||
}
|
}
|
||||||
@ -316,8 +456,8 @@ public class AdminTests extends Common {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ValidationResult groupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
|
private ValidationResult groupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member, int timeToLive) throws DataException {
|
||||||
GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing", 0);
|
GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing", timeToLive);
|
||||||
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
|
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
|
||||||
|
|
||||||
if (result == ValidationResult.OK)
|
if (result == ValidationResult.OK)
|
||||||
|
388
src/test/java/org/qortal/test/group/DevGroupAdminTests.java
Normal file
388
src/test/java/org/qortal/test/group/DevGroupAdminTests.java
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
package org.qortal.test.group;
|
||||||
|
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
|
import org.qortal.data.transaction.*;
|
||||||
|
import org.qortal.group.Group;
|
||||||
|
import org.qortal.group.Group.ApprovalThreshold;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
import org.qortal.test.common.BlockUtils;
|
||||||
|
import org.qortal.test.common.Common;
|
||||||
|
import org.qortal.test.common.GroupUtils;
|
||||||
|
import org.qortal.test.common.TransactionUtils;
|
||||||
|
import org.qortal.test.common.transaction.TestTransaction;
|
||||||
|
import org.qortal.transaction.Transaction;
|
||||||
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dev group admin tests
|
||||||
|
*
|
||||||
|
* The dev group (ID 1) is owned by the null account with public key 11111111111111111111111111111111
|
||||||
|
* To regain access to otherwise blocked owner-based rules, it has different validation logic
|
||||||
|
* which applies to groups with this same null owner.
|
||||||
|
*
|
||||||
|
* The main difference is that approval is required for certain transaction types relating to
|
||||||
|
* null-owned groups. This allows existing admins to approve updates to the group (using group's
|
||||||
|
* approval threshold) instead of these actions being performed by the owner.
|
||||||
|
*
|
||||||
|
* Since these apply to all null-owned groups, this allows anyone to update their group to
|
||||||
|
* the null owner if they want to take advantage of this decentralized approval system.
|
||||||
|
*
|
||||||
|
* Currently, the affected transaction types are:
|
||||||
|
* - AddGroupAdminTransaction
|
||||||
|
* - RemoveGroupAdminTransaction
|
||||||
|
*
|
||||||
|
* This same approach could ultimately be applied to other group transactions too.
|
||||||
|
*/
|
||||||
|
public class DevGroupAdminTests extends Common {
|
||||||
|
|
||||||
|
private static final int DEV_GROUP_ID = 1;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void beforeTest() throws DataException {
|
||||||
|
Common.useDefaultSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void afterTest() throws DataException {
|
||||||
|
Common.orphanCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupKickMember() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||||
|
|
||||||
|
// Dev group
|
||||||
|
int groupId = DEV_GROUP_ID;
|
||||||
|
|
||||||
|
// Confirm Bob is not a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Attempt to kick Bob
|
||||||
|
ValidationResult result = groupKick(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Alice to invite Bob, as it's a closed group
|
||||||
|
groupInvite(repository, alice, groupId, bob.getAddress(), 3600);
|
||||||
|
|
||||||
|
// Bob to join
|
||||||
|
joinGroup(repository, bob, groupId);
|
||||||
|
|
||||||
|
// Confirm Bob now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Attempt to kick Bob
|
||||||
|
result = groupKick(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Confirm Bob no longer a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Orphan last block
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
|
||||||
|
// Confirm Bob now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupKickAdmin() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||||
|
|
||||||
|
// Dev group
|
||||||
|
int groupId = DEV_GROUP_ID;
|
||||||
|
|
||||||
|
// Confirm Bob is not a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Alice to invite Bob, as it's a closed group
|
||||||
|
groupInvite(repository, alice, groupId, bob.getAddress(), 3600);
|
||||||
|
|
||||||
|
// Bob to join
|
||||||
|
joinGroup(repository, bob, groupId);
|
||||||
|
|
||||||
|
// Confirm Bob now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Promote Bob to admin
|
||||||
|
TransactionData addGroupAdminTransactionData = addGroupAdmin(repository, alice, groupId, bob.getAddress());
|
||||||
|
|
||||||
|
// Confirm transaction needs approval, and hasn't been approved
|
||||||
|
Transaction.ApprovalStatus approvalStatus = GroupUtils.getApprovalStatus(repository, addGroupAdminTransactionData.getSignature());
|
||||||
|
assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus);
|
||||||
|
|
||||||
|
// Have Alice approve Bob's approval-needed transaction
|
||||||
|
GroupUtils.approveTransaction(repository, "alice", addGroupAdminTransactionData.getSignature(), true);
|
||||||
|
|
||||||
|
// Mint a block so that the transaction becomes approved
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
// Confirm transaction is approved
|
||||||
|
approvalStatus = GroupUtils.getApprovalStatus(repository, addGroupAdminTransactionData.getSignature());
|
||||||
|
assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.APPROVED, approvalStatus);
|
||||||
|
|
||||||
|
// Confirm Bob is now admin
|
||||||
|
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Attempt to kick Bob
|
||||||
|
ValidationResult result = groupKick(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Shouldn't be allowed
|
||||||
|
assertEquals(ValidationResult.INVALID_GROUP_OWNER, result);
|
||||||
|
|
||||||
|
// Confirm Bob is still a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Confirm Bob still an admin
|
||||||
|
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Orphan last block
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
|
||||||
|
// Confirm Bob no longer an admin (ADD_GROUP_ADMIN no longer approved)
|
||||||
|
assertFalse(isAdmin(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Have Alice try to kick herself!
|
||||||
|
result = groupKick(repository, alice, groupId, alice.getAddress());
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Have Bob try to kick Alice
|
||||||
|
result = groupKick(repository, bob, groupId, alice.getAddress());
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupBanMember() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||||
|
|
||||||
|
// Dev group
|
||||||
|
int groupId = DEV_GROUP_ID;
|
||||||
|
|
||||||
|
// Confirm Bob is not a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Attempt to cancel non-existent Bob ban
|
||||||
|
ValidationResult result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Attempt to ban Bob
|
||||||
|
result = groupBan(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Bob attempts to rejoin
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Orphan last block (Bob ban)
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
// Delete unconfirmed group-ban transaction
|
||||||
|
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||||
|
|
||||||
|
// Confirm Bob is not a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Alice to invite Bob, as it's a closed group
|
||||||
|
groupInvite(repository, alice, groupId, bob.getAddress(), 3600);
|
||||||
|
|
||||||
|
// Bob to join
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Confirm Bob now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Attempt to ban Bob
|
||||||
|
result = groupBan(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Confirm Bob no longer a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Bob attempts to rejoin
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Cancel Bob's ban
|
||||||
|
result = cancelGroupBan(repository, alice, groupId, bob.getAddress());
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Bob attempts to rejoin
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Orphan last block (Bob join)
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
// Delete unconfirmed join-group transaction
|
||||||
|
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||||
|
|
||||||
|
// Orphan last block (Cancel Bob ban)
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
// Delete unconfirmed cancel-ban transaction
|
||||||
|
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||||
|
|
||||||
|
// Bob attempts to rejoin
|
||||||
|
result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Orphan last block (Bob ban)
|
||||||
|
BlockUtils.orphanLastBlock(repository);
|
||||||
|
// Delete unconfirmed group-ban transaction
|
||||||
|
TransactionUtils.deleteUnconfirmedTransactions(repository);
|
||||||
|
|
||||||
|
// Confirm Bob now a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGroupBanAdmin() throws DataException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
|
||||||
|
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
|
||||||
|
|
||||||
|
// Dev group
|
||||||
|
int groupId = DEV_GROUP_ID;
|
||||||
|
|
||||||
|
// Confirm Bob is not a member
|
||||||
|
assertFalse(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Alice to invite Bob, as it's a closed group
|
||||||
|
groupInvite(repository, alice, groupId, bob.getAddress(), 3600);
|
||||||
|
|
||||||
|
// Bob to join
|
||||||
|
ValidationResult result = joinGroup(repository, bob, groupId);
|
||||||
|
// Should be OK
|
||||||
|
assertEquals(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Promote Bob to admin
|
||||||
|
TransactionData addGroupAdminTransactionData = addGroupAdmin(repository, alice, groupId, bob.getAddress());
|
||||||
|
|
||||||
|
// Confirm transaction needs approval, and hasn't been approved
|
||||||
|
Transaction.ApprovalStatus approvalStatus = GroupUtils.getApprovalStatus(repository, addGroupAdminTransactionData.getSignature());
|
||||||
|
assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus);
|
||||||
|
|
||||||
|
// Have Alice approve Bob's approval-needed transaction
|
||||||
|
GroupUtils.approveTransaction(repository, "alice", addGroupAdminTransactionData.getSignature(), true);
|
||||||
|
|
||||||
|
// Mint a block so that the transaction becomes approved
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
// Confirm transaction is approved
|
||||||
|
approvalStatus = GroupUtils.getApprovalStatus(repository, addGroupAdminTransactionData.getSignature());
|
||||||
|
assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.APPROVED, approvalStatus);
|
||||||
|
|
||||||
|
// Confirm Bob is now admin
|
||||||
|
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Attempt to ban Bob
|
||||||
|
result = groupBan(repository, alice, groupId, bob.getAddress());
|
||||||
|
// .. but we can't, because Bob is an admin and the group has no owner
|
||||||
|
assertEquals(ValidationResult.INVALID_GROUP_OWNER, result);
|
||||||
|
|
||||||
|
// Confirm Bob still a member
|
||||||
|
assertTrue(isMember(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// ... and still an admin
|
||||||
|
assertTrue(isAdmin(repository, bob.getAddress(), groupId));
|
||||||
|
|
||||||
|
// Have Alice try to ban herself!
|
||||||
|
result = groupBan(repository, alice, groupId, alice.getAddress());
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
|
||||||
|
// Have Bob try to ban Alice
|
||||||
|
result = groupBan(repository, bob, groupId, alice.getAddress());
|
||||||
|
// Should NOT be OK
|
||||||
|
assertNotSame(ValidationResult.OK, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ValidationResult joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException {
|
||||||
|
JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId);
|
||||||
|
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, joiner);
|
||||||
|
|
||||||
|
if (result == ValidationResult.OK)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException {
|
||||||
|
GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin), groupId, invitee, timeToLive);
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, admin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidationResult groupKick(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
|
||||||
|
GroupKickTransactionData transactionData = new GroupKickTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing");
|
||||||
|
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
|
||||||
|
|
||||||
|
if (result == ValidationResult.OK)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidationResult groupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
|
||||||
|
GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing", 0);
|
||||||
|
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
|
||||||
|
|
||||||
|
if (result == ValidationResult.OK)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValidationResult cancelGroupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException {
|
||||||
|
CancelGroupBanTransactionData transactionData = new CancelGroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member);
|
||||||
|
ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin);
|
||||||
|
|
||||||
|
if (result == ValidationResult.OK)
|
||||||
|
BlockUtils.mintBlock(repository);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TransactionData addGroupAdmin(Repository repository, PrivateKeyAccount owner, int groupId, String member) throws DataException {
|
||||||
|
AddGroupAdminTransactionData transactionData = new AddGroupAdminTransactionData(TestTransaction.generateBase(owner), groupId, member);
|
||||||
|
transactionData.setTxGroupId(groupId);
|
||||||
|
TransactionUtils.signAndMint(repository, transactionData, owner);
|
||||||
|
return transactionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isMember(Repository repository, String address, int groupId) throws DataException {
|
||||||
|
return repository.getGroupRepository().memberExists(groupId, address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAdmin(Repository repository, String address, int groupId) throws DataException {
|
||||||
|
return repository.getGroupRepository().adminExists(groupId, address);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package org.qortal.test.at;
|
package org.qortal.test.serialization;
|
||||||
|
|
||||||
import com.google.common.hash.HashCode;
|
import com.google.common.hash.HashCode;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
@ -0,0 +1,102 @@
|
|||||||
|
package org.qortal.test.serialization;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
|
import org.qortal.data.transaction.ChatTransactionData;
|
||||||
|
import org.qortal.data.transaction.TransactionData;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.repository.RepositoryManager;
|
||||||
|
import org.qortal.test.common.Common;
|
||||||
|
import org.qortal.test.common.transaction.ChatTestTransaction;
|
||||||
|
import org.qortal.transaction.Transaction;
|
||||||
|
import org.qortal.transform.TransformationException;
|
||||||
|
import org.qortal.transform.transaction.TransactionTransformer;
|
||||||
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
public class ChatSerializationTests {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void beforeTest() throws DataException {
|
||||||
|
Common.useDefaultSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testChatSerializationWithChatReference() throws DataException, TransformationException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
// Build MESSAGE-type AT transaction with chatReference
|
||||||
|
PrivateKeyAccount signingAccount = Common.getTestAccount(repository, "alice");
|
||||||
|
ChatTransactionData transactionData = (ChatTransactionData) ChatTestTransaction.randomTransaction(repository, signingAccount, true);
|
||||||
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
|
transaction.sign(signingAccount);
|
||||||
|
|
||||||
|
assertNotNull(transactionData.getChatReference());
|
||||||
|
|
||||||
|
final int claimedLength = TransactionTransformer.getDataLength(transactionData);
|
||||||
|
byte[] serializedTransaction = TransactionTransformer.toBytes(transactionData);
|
||||||
|
assertEquals("Serialized CHAT transaction length differs from declared length", claimedLength, serializedTransaction.length);
|
||||||
|
|
||||||
|
TransactionData deserializedTransactionData = TransactionTransformer.fromBytes(serializedTransaction);
|
||||||
|
// Re-sign
|
||||||
|
Transaction deserializedTransaction = Transaction.fromData(repository, deserializedTransactionData);
|
||||||
|
deserializedTransaction.sign(signingAccount);
|
||||||
|
assertEquals("Deserialized CHAT transaction signature differs", Base58.encode(transactionData.getSignature()), Base58.encode(deserializedTransactionData.getSignature()));
|
||||||
|
|
||||||
|
// Re-serialize to check new length and bytes
|
||||||
|
final int reclaimedLength = TransactionTransformer.getDataLength(deserializedTransactionData);
|
||||||
|
assertEquals("Reserialized CHAT transaction declared length differs", claimedLength, reclaimedLength);
|
||||||
|
|
||||||
|
byte[] reserializedTransaction = TransactionTransformer.toBytes(deserializedTransactionData);
|
||||||
|
assertEquals("Reserialized CHAT transaction bytes differ", HashCode.fromBytes(serializedTransaction).toString(), HashCode.fromBytes(reserializedTransaction).toString());
|
||||||
|
|
||||||
|
// Deserialized chat reference must match initial chat reference
|
||||||
|
ChatTransactionData deserializedChatTransactionData = (ChatTransactionData) deserializedTransactionData;
|
||||||
|
assertNotNull(deserializedChatTransactionData.getChatReference());
|
||||||
|
assertArrayEquals(deserializedChatTransactionData.getChatReference(), transactionData.getChatReference());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testChatSerializationWithoutChatReference() throws DataException, TransformationException {
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
|
||||||
|
// Build MESSAGE-type AT transaction without chatReference
|
||||||
|
PrivateKeyAccount signingAccount = Common.getTestAccount(repository, "alice");
|
||||||
|
ChatTransactionData transactionData = (ChatTransactionData) ChatTestTransaction.randomTransaction(repository, signingAccount, true);
|
||||||
|
transactionData.setChatReference(null);
|
||||||
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
|
transaction.sign(signingAccount);
|
||||||
|
|
||||||
|
assertNull(transactionData.getChatReference());
|
||||||
|
|
||||||
|
final int claimedLength = TransactionTransformer.getDataLength(transactionData);
|
||||||
|
byte[] serializedTransaction = TransactionTransformer.toBytes(transactionData);
|
||||||
|
assertEquals("Serialized CHAT transaction length differs from declared length", claimedLength, serializedTransaction.length);
|
||||||
|
|
||||||
|
TransactionData deserializedTransactionData = TransactionTransformer.fromBytes(serializedTransaction);
|
||||||
|
// Re-sign
|
||||||
|
Transaction deserializedTransaction = Transaction.fromData(repository, deserializedTransactionData);
|
||||||
|
deserializedTransaction.sign(signingAccount);
|
||||||
|
assertEquals("Deserialized CHAT transaction signature differs", Base58.encode(transactionData.getSignature()), Base58.encode(deserializedTransactionData.getSignature()));
|
||||||
|
|
||||||
|
// Re-serialize to check new length and bytes
|
||||||
|
final int reclaimedLength = TransactionTransformer.getDataLength(deserializedTransactionData);
|
||||||
|
assertEquals("Reserialized CHAT transaction declared length differs", claimedLength, reclaimedLength);
|
||||||
|
|
||||||
|
byte[] reserializedTransaction = TransactionTransformer.toBytes(deserializedTransactionData);
|
||||||
|
assertEquals("Reserialized CHAT transaction bytes differ", HashCode.fromBytes(serializedTransaction).toString(), HashCode.fromBytes(reserializedTransaction).toString());
|
||||||
|
|
||||||
|
// Deserialized chat reference must match initial chat reference
|
||||||
|
ChatTransactionData deserializedChatTransactionData = (ChatTransactionData) deserializedTransactionData;
|
||||||
|
assertNull(deserializedChatTransactionData.getChatReference());
|
||||||
|
assertArrayEquals(deserializedChatTransactionData.getChatReference(), transactionData.getChatReference());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package org.qortal.test;
|
package org.qortal.test.serialization;
|
||||||
|
|
||||||
import org.junit.Ignore;
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -47,7 +47,6 @@ public class SerializationTests extends Common {
|
|||||||
switch (txType) {
|
switch (txType) {
|
||||||
case GENESIS:
|
case GENESIS:
|
||||||
case ACCOUNT_FLAGS:
|
case ACCOUNT_FLAGS:
|
||||||
case CHAT:
|
|
||||||
case PUBLICIZE:
|
case PUBLICIZE:
|
||||||
case AIRDROP:
|
case AIRDROP:
|
||||||
case ENABLE_FORGING:
|
case ENABLE_FORGING:
|
||||||
@ -60,6 +59,7 @@ public class SerializationTests extends Common {
|
|||||||
TransactionData transactionData = TransactionUtils.randomTransaction(repository, signingAccount, txType, true);
|
TransactionData transactionData = TransactionUtils.randomTransaction(repository, signingAccount, txType, true);
|
||||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
transaction.sign(signingAccount);
|
transaction.sign(signingAccount);
|
||||||
|
transaction.importAsUnconfirmed();
|
||||||
|
|
||||||
final int claimedLength = TransactionTransformer.getDataLength(transactionData);
|
final int claimedLength = TransactionTransformer.getDataLength(transactionData);
|
||||||
byte[] serializedTransaction = TransactionTransformer.toBytes(transactionData);
|
byte[] serializedTransaction = TransactionTransformer.toBytes(transactionData);
|
@ -14,6 +14,7 @@
|
|||||||
"founderEffectiveMintingLevel": 10,
|
"founderEffectiveMintingLevel": 10,
|
||||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||||
"rewardsByHeight": [
|
"rewardsByHeight": [
|
||||||
{ "height": 1, "reward": 100 },
|
{ "height": 1, "reward": 100 },
|
||||||
{ "height": 11, "reward": 10 },
|
{ "height": 11, "reward": 10 },
|
||||||
@ -70,7 +71,11 @@
|
|||||||
"transactionV5Timestamp": 0,
|
"transactionV5Timestamp": 0,
|
||||||
"transactionV6Timestamp": 9999999999999,
|
"transactionV6Timestamp": 9999999999999,
|
||||||
"disableReferenceTimestamp": 9999999999999,
|
"disableReferenceTimestamp": 9999999999999,
|
||||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 0,
|
||||||
|
"selfSponsorshipAlgoV1Height": 999999999,
|
||||||
|
"feeValidationFixTimestamp": 0,
|
||||||
|
"chatReferenceTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"founderEffectiveMintingLevel": 10,
|
"founderEffectiveMintingLevel": 10,
|
||||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||||
"rewardsByHeight": [
|
"rewardsByHeight": [
|
||||||
{ "height": 1, "reward": 100 },
|
{ "height": 1, "reward": 100 },
|
||||||
{ "height": 11, "reward": 10 },
|
{ "height": 11, "reward": 10 },
|
||||||
@ -73,7 +74,11 @@
|
|||||||
"transactionV5Timestamp": 0,
|
"transactionV5Timestamp": 0,
|
||||||
"transactionV6Timestamp": 0,
|
"transactionV6Timestamp": 0,
|
||||||
"disableReferenceTimestamp": 0,
|
"disableReferenceTimestamp": 0,
|
||||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 0,
|
||||||
|
"selfSponsorshipAlgoV1Height": 999999999,
|
||||||
|
"feeValidationFixTimestamp": 0,
|
||||||
|
"chatReferenceTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||||
"rewardsByHeight": [
|
"rewardsByHeight": [
|
||||||
{ "height": 1, "reward": 100 },
|
{ "height": 1, "reward": 100 },
|
||||||
{ "height": 11, "reward": 10 },
|
{ "height": 11, "reward": 10 },
|
||||||
@ -74,7 +75,11 @@
|
|||||||
"transactionV5Timestamp": 0,
|
"transactionV5Timestamp": 0,
|
||||||
"transactionV6Timestamp": 0,
|
"transactionV6Timestamp": 0,
|
||||||
"disableReferenceTimestamp": 9999999999999,
|
"disableReferenceTimestamp": 9999999999999,
|
||||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 0,
|
||||||
|
"selfSponsorshipAlgoV1Height": 999999999,
|
||||||
|
"feeValidationFixTimestamp": 0,
|
||||||
|
"chatReferenceTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||||
"rewardsByHeight": [
|
"rewardsByHeight": [
|
||||||
{ "height": 1, "reward": 100 },
|
{ "height": 1, "reward": 100 },
|
||||||
{ "height": 11, "reward": 10 },
|
{ "height": 11, "reward": 10 },
|
||||||
@ -74,7 +75,11 @@
|
|||||||
"transactionV5Timestamp": 0,
|
"transactionV5Timestamp": 0,
|
||||||
"transactionV6Timestamp": 0,
|
"transactionV6Timestamp": 0,
|
||||||
"disableReferenceTimestamp": 9999999999999,
|
"disableReferenceTimestamp": 9999999999999,
|
||||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 0,
|
||||||
|
"selfSponsorshipAlgoV1Height": 999999999,
|
||||||
|
"feeValidationFixTimestamp": 0,
|
||||||
|
"chatReferenceTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||||
"rewardsByHeight": [
|
"rewardsByHeight": [
|
||||||
{ "height": 1, "reward": 100 },
|
{ "height": 1, "reward": 100 },
|
||||||
{ "height": 11, "reward": 10 },
|
{ "height": 11, "reward": 10 },
|
||||||
@ -74,7 +75,11 @@
|
|||||||
"transactionV5Timestamp": 0,
|
"transactionV5Timestamp": 0,
|
||||||
"transactionV6Timestamp": 0,
|
"transactionV6Timestamp": 0,
|
||||||
"disableReferenceTimestamp": 9999999999999,
|
"disableReferenceTimestamp": 9999999999999,
|
||||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 0,
|
||||||
|
"selfSponsorshipAlgoV1Height": 999999999,
|
||||||
|
"feeValidationFixTimestamp": 0,
|
||||||
|
"chatReferenceTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||||
"rewardsByHeight": [
|
"rewardsByHeight": [
|
||||||
{ "height": 1, "reward": 100 },
|
{ "height": 1, "reward": 100 },
|
||||||
{ "height": 11, "reward": 10 },
|
{ "height": 11, "reward": 10 },
|
||||||
@ -74,7 +75,11 @@
|
|||||||
"transactionV5Timestamp": 0,
|
"transactionV5Timestamp": 0,
|
||||||
"transactionV6Timestamp": 0,
|
"transactionV6Timestamp": 0,
|
||||||
"disableReferenceTimestamp": 9999999999999,
|
"disableReferenceTimestamp": 9999999999999,
|
||||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 0,
|
||||||
|
"selfSponsorshipAlgoV1Height": 999999999,
|
||||||
|
"feeValidationFixTimestamp": 0,
|
||||||
|
"chatReferenceTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||||
"rewardsByHeight": [
|
"rewardsByHeight": [
|
||||||
{ "height": 1, "reward": 100 },
|
{ "height": 1, "reward": 100 },
|
||||||
{ "height": 11, "reward": 10 },
|
{ "height": 11, "reward": 10 },
|
||||||
@ -75,7 +76,11 @@
|
|||||||
"transactionV6Timestamp": 0,
|
"transactionV6Timestamp": 0,
|
||||||
"disableReferenceTimestamp": 9999999999999,
|
"disableReferenceTimestamp": 9999999999999,
|
||||||
"aggregateSignatureTimestamp": 0,
|
"aggregateSignatureTimestamp": 0,
|
||||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 0,
|
||||||
|
"selfSponsorshipAlgoV1Height": 999999999,
|
||||||
|
"feeValidationFixTimestamp": 0,
|
||||||
|
"chatReferenceTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||||
"rewardsByHeight": [
|
"rewardsByHeight": [
|
||||||
{ "height": 1, "reward": 100 },
|
{ "height": 1, "reward": 100 },
|
||||||
{ "height": 11, "reward": 10 },
|
{ "height": 11, "reward": 10 },
|
||||||
@ -74,7 +75,11 @@
|
|||||||
"transactionV5Timestamp": 0,
|
"transactionV5Timestamp": 0,
|
||||||
"transactionV6Timestamp": 0,
|
"transactionV6Timestamp": 0,
|
||||||
"disableReferenceTimestamp": 9999999999999,
|
"disableReferenceTimestamp": 9999999999999,
|
||||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 0,
|
||||||
|
"selfSponsorshipAlgoV1Height": 999999999,
|
||||||
|
"feeValidationFixTimestamp": 0,
|
||||||
|
"chatReferenceTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||||
"rewardsByHeight": [
|
"rewardsByHeight": [
|
||||||
{ "height": 1, "reward": 100 },
|
{ "height": 1, "reward": 100 },
|
||||||
{ "height": 11, "reward": 10 },
|
{ "height": 11, "reward": 10 },
|
||||||
@ -74,7 +75,11 @@
|
|||||||
"transactionV5Timestamp": 0,
|
"transactionV5Timestamp": 0,
|
||||||
"transactionV6Timestamp": 0,
|
"transactionV6Timestamp": 0,
|
||||||
"disableReferenceTimestamp": 9999999999999,
|
"disableReferenceTimestamp": 9999999999999,
|
||||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 0,
|
||||||
|
"selfSponsorshipAlgoV1Height": 999999999,
|
||||||
|
"feeValidationFixTimestamp": 0,
|
||||||
|
"chatReferenceTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||||
"rewardsByHeight": [
|
"rewardsByHeight": [
|
||||||
{ "height": 1, "reward": 100 },
|
{ "height": 1, "reward": 100 },
|
||||||
{ "height": 11, "reward": 10 },
|
{ "height": 11, "reward": 10 },
|
||||||
@ -74,7 +75,11 @@
|
|||||||
"transactionV5Timestamp": 0,
|
"transactionV5Timestamp": 0,
|
||||||
"transactionV6Timestamp": 0,
|
"transactionV6Timestamp": 0,
|
||||||
"disableReferenceTimestamp": 9999999999999,
|
"disableReferenceTimestamp": 9999999999999,
|
||||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 0,
|
||||||
|
"selfSponsorshipAlgoV1Height": 999999999,
|
||||||
|
"feeValidationFixTimestamp": 0,
|
||||||
|
"chatReferenceTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"founderEffectiveMintingLevel": 10,
|
"founderEffectiveMintingLevel": 10,
|
||||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||||
"rewardsByHeight": [
|
"rewardsByHeight": [
|
||||||
{ "height": 1, "reward": 100 },
|
{ "height": 1, "reward": 100 },
|
||||||
{ "height": 11, "reward": 10 },
|
{ "height": 11, "reward": 10 },
|
||||||
@ -74,7 +75,11 @@
|
|||||||
"transactionV5Timestamp": 0,
|
"transactionV5Timestamp": 0,
|
||||||
"transactionV6Timestamp": 0,
|
"transactionV6Timestamp": 0,
|
||||||
"disableReferenceTimestamp": 9999999999999,
|
"disableReferenceTimestamp": 9999999999999,
|
||||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 0,
|
||||||
|
"selfSponsorshipAlgoV1Height": 999999999,
|
||||||
|
"feeValidationFixTimestamp": 0,
|
||||||
|
"chatReferenceTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
|
116
src/test/resources/test-chain-v2-self-sponsorship-algo.json
Normal file
116
src/test/resources/test-chain-v2-self-sponsorship-algo.json
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
{
|
||||||
|
"isTestChain": true,
|
||||||
|
"blockTimestampMargin": 500,
|
||||||
|
"transactionExpiryPeriod": 86400000,
|
||||||
|
"maxBlockSize": 2097152,
|
||||||
|
"maxBytesPerUnitFee": 0,
|
||||||
|
"unitFee": "0.00000001",
|
||||||
|
"nameRegistrationUnitFees": [
|
||||||
|
{ "timestamp": 1645372800000, "fee": "5" }
|
||||||
|
],
|
||||||
|
"requireGroupForApproval": false,
|
||||||
|
"minAccountLevelToRewardShare": 5,
|
||||||
|
"maxRewardSharesPerFounderMintingAccount": 20,
|
||||||
|
"maxRewardSharesByTimestamp": [
|
||||||
|
{ "timestamp": 0, "maxShares": 20 },
|
||||||
|
{ "timestamp": 9999999999999, "maxShares": 3 }
|
||||||
|
],
|
||||||
|
"founderEffectiveMintingLevel": 10,
|
||||||
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
|
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||||
|
"rewardsByHeight": [
|
||||||
|
{ "height": 1, "reward": 100 },
|
||||||
|
{ "height": 11, "reward": 10 },
|
||||||
|
{ "height": 21, "reward": 1 }
|
||||||
|
],
|
||||||
|
"sharesByLevelV1": [
|
||||||
|
{ "id": 1, "levels": [ 1, 2 ], "share": 0.05 },
|
||||||
|
{ "id": 2, "levels": [ 3, 4 ], "share": 0.10 },
|
||||||
|
{ "id": 3, "levels": [ 5, 6 ], "share": 0.15 },
|
||||||
|
{ "id": 4, "levels": [ 7, 8 ], "share": 0.20 },
|
||||||
|
{ "id": 5, "levels": [ 9, 10 ], "share": 0.25 }
|
||||||
|
],
|
||||||
|
"sharesByLevelV2": [
|
||||||
|
{ "id": 1, "levels": [ 1, 2 ], "share": 0.06 },
|
||||||
|
{ "id": 2, "levels": [ 3, 4 ], "share": 0.13 },
|
||||||
|
{ "id": 3, "levels": [ 5, 6 ], "share": 0.19 },
|
||||||
|
{ "id": 4, "levels": [ 7, 8 ], "share": 0.26 },
|
||||||
|
{ "id": 5, "levels": [ 9, 10 ], "share": 0.32 }
|
||||||
|
],
|
||||||
|
"qoraHoldersShareByHeight": [
|
||||||
|
{ "height": 1, "share": 0.20 },
|
||||||
|
{ "height": 1000000, "share": 0.01 }
|
||||||
|
],
|
||||||
|
"qoraPerQortReward": 250,
|
||||||
|
"minAccountsToActivateShareBin": 0,
|
||||||
|
"shareBinActivationMinLevel": 7,
|
||||||
|
"blocksNeededByLevel": [ 5, 20, 30, 40, 50, 60, 18, 80, 90, 100 ],
|
||||||
|
"blockTimingsByHeight": [
|
||||||
|
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||||
|
],
|
||||||
|
"ciyamAtSettings": {
|
||||||
|
"feePerStep": "0.0001",
|
||||||
|
"maxStepsPerRound": 500,
|
||||||
|
"stepsPerFunctionCall": 10,
|
||||||
|
"minutesPerBlock": 1
|
||||||
|
},
|
||||||
|
"featureTriggers": {
|
||||||
|
"messageHeight": 0,
|
||||||
|
"atHeight": 0,
|
||||||
|
"assetsTimestamp": 0,
|
||||||
|
"votingTimestamp": 0,
|
||||||
|
"arbitraryTimestamp": 0,
|
||||||
|
"powfixTimestamp": 0,
|
||||||
|
"qortalTimestamp": 0,
|
||||||
|
"newAssetPricingTimestamp": 0,
|
||||||
|
"groupApprovalTimestamp": 0,
|
||||||
|
"atFindNextTransactionFix": 0,
|
||||||
|
"newBlockSigHeight": 999999,
|
||||||
|
"shareBinFix": 999999,
|
||||||
|
"sharesByLevelV2Height": 999999,
|
||||||
|
"rewardShareLimitTimestamp": 9999999999999,
|
||||||
|
"calcChainWeightTimestamp": 0,
|
||||||
|
"transactionV5Timestamp": 0,
|
||||||
|
"transactionV6Timestamp": 0,
|
||||||
|
"disableReferenceTimestamp": 9999999999999,
|
||||||
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 0,
|
||||||
|
"selfSponsorshipAlgoV1Height": 20,
|
||||||
|
"feeValidationFixTimestamp": 0,
|
||||||
|
"chatReferenceTimestamp": 0
|
||||||
|
},
|
||||||
|
"genesisInfo": {
|
||||||
|
"version": 4,
|
||||||
|
"timestamp": 0,
|
||||||
|
"transactions": [
|
||||||
|
{ "type": "ISSUE_ASSET", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0 },
|
||||||
|
{ "type": "ISSUE_ASSET", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||||
|
{ "type": "ISSUE_ASSET", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
|
||||||
|
|
||||||
|
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" },
|
||||||
|
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" },
|
||||||
|
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" },
|
||||||
|
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" },
|
||||||
|
|
||||||
|
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
|
||||||
|
|
||||||
|
{ "type": "UPDATE_GROUP", "ownerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupId": 1, "newOwner": "QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG", "newDescription": "developer group", "newIsOpen": false, "newApprovalThreshold": "PCT40", "minimumBlockDelay": 10, "maximumBlockDelay": 1440 },
|
||||||
|
|
||||||
|
{ "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
||||||
|
{ "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
||||||
|
{ "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
||||||
|
|
||||||
|
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
|
||||||
|
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": "100" },
|
||||||
|
|
||||||
|
{ "type": "ACCOUNT_LEVEL", "target": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "level": 5 },
|
||||||
|
{ "type": "REWARD_SHARE", "minterPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "rewardSharePublicKey": "CcABzvk26TFEHG7Yok84jxyd4oBtLkx8RJdGFVz2csvp", "sharePercent": 100 },
|
||||||
|
|
||||||
|
{ "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 5 },
|
||||||
|
{ "type": "ACCOUNT_LEVEL", "target": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "level": 5 },
|
||||||
|
{ "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 6 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@
|
|||||||
"onlineAccountSignaturesMinLifetime": 3600000,
|
"onlineAccountSignaturesMinLifetime": 3600000,
|
||||||
"onlineAccountSignaturesMaxLifetime": 86400000,
|
"onlineAccountSignaturesMaxLifetime": 86400000,
|
||||||
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
"onlineAccountsModulusV2Timestamp": 9999999999999,
|
||||||
|
"selfSponsorshipAlgoV1SnapshotTimestamp": 9999999999999,
|
||||||
"rewardsByHeight": [
|
"rewardsByHeight": [
|
||||||
{ "height": 1, "reward": 100 },
|
{ "height": 1, "reward": 100 },
|
||||||
{ "height": 11, "reward": 10 },
|
{ "height": 11, "reward": 10 },
|
||||||
@ -74,7 +75,11 @@
|
|||||||
"transactionV5Timestamp": 0,
|
"transactionV5Timestamp": 0,
|
||||||
"transactionV6Timestamp": 0,
|
"transactionV6Timestamp": 0,
|
||||||
"disableReferenceTimestamp": 9999999999999,
|
"disableReferenceTimestamp": 9999999999999,
|
||||||
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999
|
"increaseOnlineAccountsDifficultyTimestamp": 9999999999999,
|
||||||
|
"onlineAccountMinterLevelValidationHeight": 0,
|
||||||
|
"selfSponsorshipAlgoV1Height": 999999999,
|
||||||
|
"feeValidationFixTimestamp": 0,
|
||||||
|
"chatReferenceTimestamp": 0
|
||||||
},
|
},
|
||||||
"genesisInfo": {
|
"genesisInfo": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
@ -91,6 +96,8 @@
|
|||||||
|
|
||||||
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
|
{ "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 },
|
||||||
|
|
||||||
|
{ "type": "UPDATE_GROUP", "ownerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupId": 1, "newOwner": "QdSnUy6sUiEnaN87dWmE92g1uQjrvPgrWG", "newDescription": "developer group", "newIsOpen": false, "newApprovalThreshold": "PCT40", "minimumBlockDelay": 10, "maximumBlockDelay": 1440 },
|
||||||
|
|
||||||
{ "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
{ "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
||||||
{ "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
{ "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
||||||
{ "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
{ "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": "1000000", "isDivisible": true, "fee": 0 },
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"repositoryPath": "testdb",
|
||||||
|
"bitcoinNet": "TEST3",
|
||||||
|
"litecoinNet": "TEST3",
|
||||||
|
"restrictedApi": false,
|
||||||
|
"blockchainConfig": "src/test/resources/test-chain-v2-self-sponsorship-algo.json",
|
||||||
|
"exportPath": "qortal-backup-test",
|
||||||
|
"bootstrap": false,
|
||||||
|
"wipeUnconfirmedOnStart": false,
|
||||||
|
"testNtpOffset": 0,
|
||||||
|
"minPeers": 0,
|
||||||
|
"pruneBlockLimit": 100,
|
||||||
|
"bootstrapFilenamePrefix": "test-",
|
||||||
|
"dataPath": "data-test",
|
||||||
|
"tempDataPath": "data-test/_temp",
|
||||||
|
"listsPath": "lists-test",
|
||||||
|
"storagePolicy": "FOLLOWED_OR_VIEWED",
|
||||||
|
"maxStorageCapacity": 104857600,
|
||||||
|
"arrrDefaultBirthday": 1900000
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user