forked from Qortal/qortal
Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cd7adc997b | ||
|
9fdc901b7a | ||
|
b29ae67501 | ||
|
24f1fb566d | ||
|
a253294890 | ||
|
ec008b4a16 | ||
|
1d65e34fe5 | ||
|
8ae78703ca | ||
|
bd4b9a9fd3 | ||
|
f09677d376 | ||
|
f669e3f6c4 | ||
|
961c5ea962 | ||
|
a1c61a1146 | ||
|
797dff4752 | ||
|
711ad638b8 | ||
|
4956c3328c | ||
|
68190c8c76 | ||
|
dde47bc1fc | ||
|
744deaed8d | ||
|
a62910c8b6 | ||
|
a9c7142d7b | ||
|
7a40c3526f | ||
|
3253d9d3fb |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/db*
|
||||
/lists/
|
||||
/bin/
|
||||
/target/
|
||||
/qortal-backup/
|
||||
@@ -15,8 +16,8 @@
|
||||
/settings.json
|
||||
/testnet*
|
||||
/settings*.json
|
||||
/testchain.json
|
||||
/run-testnet.sh
|
||||
/testchain*.json
|
||||
/run-testnet*.sh
|
||||
/.idea
|
||||
/qortal.iml
|
||||
.DS_Store
|
||||
|
@@ -17,10 +17,10 @@
|
||||
<ROW Property="Manufacturer" Value="Qortal"/>
|
||||
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
||||
<ROW Property="NTP_GOOD" Value="false"/>
|
||||
<ROW Property="ProductCode" Value="1033:{E67F58ED-2236-43A9-8895-B9AB96C60EB9} 1049:{E612D7B8-EB9F-481C-81FF-7530E0801F95} 2052:{8EC6F665-1D21-4DB8-8C55-05C2550FF1B3} 2057:{6CE725B6-BBDD-459D-8016-6D1D2FC1F4EC} " Type="16"/>
|
||||
<ROW Property="ProductCode" Value="1033:{482E9390-1005-42FD-9F3F-E160E0E6FB19} 1049:{8FE09AC2-814B-42FC-9FCE-53D45A396529} 2052:{4FABD326-8345-438B-82B8-66C2DC3676E6} 2057:{7ECFFF43-DEC7-4B7F-BC88-260A10AF132A} " Type="16"/>
|
||||
<ROW Property="ProductLanguage" Value="2057"/>
|
||||
<ROW Property="ProductName" Value="Qortal"/>
|
||||
<ROW Property="ProductVersion" Value="1.5.6" Type="32"/>
|
||||
<ROW Property="ProductVersion" Value="1.6.0" Type="32"/>
|
||||
<ROW Property="RECONFIG_NTP" Value="true"/>
|
||||
<ROW Property="REMOVE_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_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="AI_CustomARPName" ComponentId="{9DA2985C-778C-4D85-A44E-5B00D935EED2}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{7941AD6C-7C09-48E7-93ED-0340E0F52EC0}" 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="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"/>
|
||||
|
18
src/main/java/org/qortal/api/model/AddressListRequest.java
Normal file
18
src/main/java/org/qortal/api/model/AddressListRequest.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.util.List;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class AddressListRequest {
|
||||
|
||||
@Schema(description = "A list of addresses")
|
||||
public List<String> addresses;
|
||||
|
||||
public AddressListRequest() {
|
||||
}
|
||||
|
||||
}
|
271
src/main/java/org/qortal/api/resource/ListsResource.java
Normal file
271
src/main/java/org/qortal/api/resource/ListsResource.java
Normal file
@@ -0,0 +1,271 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import org.qortal.api.*;
|
||||
import org.qortal.api.model.AddressListRequest;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.account.AccountData;
|
||||
import org.qortal.list.ResourceListManager;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
|
||||
@Path("/lists")
|
||||
@Tag(name = "Lists")
|
||||
public class ListsResource {
|
||||
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
|
||||
@POST
|
||||
@Path("/blacklist/address/{address}")
|
||||
@Operation(
|
||||
summary = "Add a QORT address to the local blacklist",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "Returns true on success, or an exception on failure",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public String addAddressToBlacklist(@PathParam("address") String address) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
// Not found?
|
||||
if (accountData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
// Valid address, so go ahead and blacklist it
|
||||
boolean success = ResourceListManager.getInstance().addAddressToBlacklist(address, true);
|
||||
|
||||
return success ? "true" : "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/blacklist/addresses")
|
||||
@Operation(
|
||||
summary = "Add one or more QORT addresses to the local blacklist",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = AddressListRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "Returns true if all addresses were processed, false if any couldn't be " +
|
||||
"processed, or an exception on failure. If false or an exception is returned, " +
|
||||
"the list will not be updated, and the request will need to be re-issued.",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public String addAddressesToBlacklist(AddressListRequest addressListRequest) {
|
||||
if (addressListRequest == null || addressListRequest.addresses == null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
|
||||
int successCount = 0;
|
||||
int errorCount = 0;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
for (String address : addressListRequest.addresses) {
|
||||
|
||||
if (!Crypto.isValidAddress(address)) {
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
// Not found?
|
||||
if (accountData == null) {
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Valid address, so go ahead and blacklist it
|
||||
boolean success = ResourceListManager.getInstance().addAddressToBlacklist(address, false);
|
||||
if (success) {
|
||||
successCount++;
|
||||
}
|
||||
else {
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
||||
if (successCount > 0 && errorCount == 0) {
|
||||
// All were successful, so save the blacklist
|
||||
ResourceListManager.getInstance().saveBlacklist();
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
// Something went wrong, so revert
|
||||
ResourceListManager.getInstance().revertBlacklist();
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@DELETE
|
||||
@Path("/blacklist/address/{address}")
|
||||
@Operation(
|
||||
summary = "Remove a QORT address from the local blacklist",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "Returns true on success, or an exception on failure",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public String removeAddressFromBlacklist(@PathParam("address") String address) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
// Not found?
|
||||
if (accountData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
// Valid address, so go ahead and blacklist it
|
||||
boolean success = ResourceListManager.getInstance().removeAddressFromBlacklist(address, true);
|
||||
|
||||
return success ? "true" : "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/blacklist/addresses")
|
||||
@Operation(
|
||||
summary = "Remove one or more QORT addresses from the local blacklist",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = AddressListRequest.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "Returns true if all addresses were processed, false if any couldn't be " +
|
||||
"processed, or an exception on failure. If false or an exception is returned, " +
|
||||
"the list will not be updated, and the request will need to be re-issued.",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public String removeAddressesFromBlacklist(AddressListRequest addressListRequest) {
|
||||
if (addressListRequest == null || addressListRequest.addresses == null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
}
|
||||
|
||||
int successCount = 0;
|
||||
int errorCount = 0;
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
for (String address : addressListRequest.addresses) {
|
||||
|
||||
if (!Crypto.isValidAddress(address)) {
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
// Not found?
|
||||
if (accountData == null) {
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Valid address, so go ahead and blacklist it
|
||||
// Don't save as we will do this at the end of the process
|
||||
boolean success = ResourceListManager.getInstance().removeAddressFromBlacklist(address, false);
|
||||
if (success) {
|
||||
successCount++;
|
||||
}
|
||||
else {
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
||||
if (successCount > 0 && errorCount == 0) {
|
||||
// All were successful, so save the blacklist
|
||||
ResourceListManager.getInstance().saveBlacklist();
|
||||
return "true";
|
||||
}
|
||||
else {
|
||||
// Something went wrong, so revert
|
||||
ResourceListManager.getInstance().revertBlacklist();
|
||||
return "false";
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/blacklist/address/{address}")
|
||||
@Operation(
|
||||
summary = "Check if an address is present in the local blacklist",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "Returns true or false if the list was queried, or an exception on failure",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
|
||||
public String checkAddressInBlacklist(@PathParam("address") String address) {
|
||||
if (!Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
AccountData accountData = repository.getAccountRepository().getAccount(address);
|
||||
// Not found?
|
||||
if (accountData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||
|
||||
// Valid address, so go ahead and blacklist it
|
||||
boolean blacklisted = ResourceListManager.getInstance().isAddressInBlacklist(address);
|
||||
|
||||
return blacklisted ? "true" : "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,5 +1,7 @@
|
||||
package org.qortal.at;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.ciyam.at.MachineState;
|
||||
@@ -56,12 +58,12 @@ public class AT {
|
||||
|
||||
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, codeBytes, codeHash,
|
||||
machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
|
||||
machineState.isFrozen(), machineState.getFrozenBalance());
|
||||
machineState.isFrozen(), machineState.getFrozenBalance(), null);
|
||||
|
||||
byte[] stateData = machineState.toBytes();
|
||||
byte[] stateHash = Crypto.digest(stateData);
|
||||
|
||||
this.atStateData = new ATStateData(atAddress, height, stateData, stateHash, 0L, true);
|
||||
this.atStateData = new ATStateData(atAddress, height, stateData, stateHash, 0L, true, null);
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@@ -84,13 +86,28 @@ public class AT {
|
||||
this.repository.getATRepository().delete(this.atData.getATAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* Potentially execute AT.
|
||||
* <p>
|
||||
* Note that sleep-until-message support might set/reset
|
||||
* sleep-related flags/values.
|
||||
* <p>
|
||||
* {@link #getATStateData()} will return null if nothing happened.
|
||||
* <p>
|
||||
* @param blockHeight
|
||||
* @param blockTimestamp
|
||||
* @return AT-generated transactions, possibly empty
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<AtTransaction> run(int blockHeight, long blockTimestamp) throws DataException {
|
||||
String atAddress = this.atData.getATAddress();
|
||||
|
||||
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
|
||||
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
|
||||
|
||||
byte[] codeBytes = this.atData.getCodeBytes();
|
||||
if (!api.willExecute(blockHeight))
|
||||
// this.atStateData will be null
|
||||
return Collections.emptyList();
|
||||
|
||||
// Fetch latest ATStateData for this AT
|
||||
ATStateData latestAtStateData = this.repository.getATRepository().getLatestATState(atAddress);
|
||||
@@ -100,8 +117,10 @@ public class AT {
|
||||
throw new IllegalStateException("No previous AT state data found");
|
||||
|
||||
// [Re]create AT machine state using AT state data or from scratch as applicable
|
||||
byte[] codeBytes = this.atData.getCodeBytes();
|
||||
MachineState state = MachineState.fromBytes(api, loggerFactory, latestAtStateData.getStateData(), codeBytes);
|
||||
try {
|
||||
api.preExecute(state);
|
||||
state.execute();
|
||||
} catch (Exception e) {
|
||||
throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e);
|
||||
@@ -109,9 +128,18 @@ public class AT {
|
||||
|
||||
byte[] stateData = state.toBytes();
|
||||
byte[] stateHash = Crypto.digest(stateData);
|
||||
long atFees = api.calcFinalFees(state);
|
||||
|
||||
this.atStateData = new ATStateData(atAddress, blockHeight, stateData, stateHash, atFees, false);
|
||||
// Nothing happened?
|
||||
if (state.getSteps() == 0 && Arrays.equals(stateHash, latestAtStateData.getStateHash()))
|
||||
// We currently want to execute frozen ATs, to maintain backwards support.
|
||||
if (state.isFrozen() == false)
|
||||
// this.atStateData will be null
|
||||
return Collections.emptyList();
|
||||
|
||||
long atFees = api.calcFinalFees(state);
|
||||
Long sleepUntilMessageTimestamp = this.atData.getSleepUntilMessageTimestamp();
|
||||
|
||||
this.atStateData = new ATStateData(atAddress, blockHeight, stateData, stateHash, atFees, false, sleepUntilMessageTimestamp);
|
||||
|
||||
return api.getTransactions();
|
||||
}
|
||||
@@ -130,6 +158,10 @@ public class AT {
|
||||
this.atData.setHadFatalError(state.hadFatalError());
|
||||
this.atData.setIsFrozen(state.isFrozen());
|
||||
this.atData.setFrozenBalance(state.getFrozenBalance());
|
||||
|
||||
// Special sleep-until-message support
|
||||
this.atData.setSleepUntilMessageTimestamp(this.atStateData.getSleepUntilMessageTimestamp());
|
||||
|
||||
this.repository.getATRepository().save(this.atData);
|
||||
}
|
||||
|
||||
@@ -157,6 +189,10 @@ public class AT {
|
||||
this.atData.setHadFatalError(state.hadFatalError());
|
||||
this.atData.setIsFrozen(state.isFrozen());
|
||||
this.atData.setFrozenBalance(state.getFrozenBalance());
|
||||
|
||||
// Special sleep-until-message support
|
||||
this.atData.setSleepUntilMessageTimestamp(previousStateData.getSleepUntilMessageTimestamp());
|
||||
|
||||
this.repository.getATRepository().save(this.atData);
|
||||
}
|
||||
|
||||
|
@@ -32,6 +32,7 @@ import org.qortal.group.Group;
|
||||
import org.qortal.repository.ATRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.ATRepository.NextTransactionInfo;
|
||||
import org.qortal.transaction.AtTransaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.utils.Base58;
|
||||
@@ -74,8 +75,45 @@ public class QortalATAPI extends API {
|
||||
return this.transactions;
|
||||
}
|
||||
|
||||
public long calcFinalFees(MachineState state) {
|
||||
return state.getSteps() * this.ciyamAtSettings.feePerStep;
|
||||
public boolean willExecute(int blockHeight) throws DataException {
|
||||
// Sleep-until-message/height checking
|
||||
Long sleepUntilMessageTimestamp = this.atData.getSleepUntilMessageTimestamp();
|
||||
|
||||
if (sleepUntilMessageTimestamp != null) {
|
||||
// Quicker to check height, if sleep-until-height also active
|
||||
Integer sleepUntilHeight = this.atData.getSleepUntilHeight();
|
||||
|
||||
boolean wakeDueToHeight = sleepUntilHeight != null && sleepUntilHeight != 0 && blockHeight >= sleepUntilHeight;
|
||||
|
||||
boolean wakeDueToMessage = false;
|
||||
if (!wakeDueToHeight) {
|
||||
// No avoiding asking repository
|
||||
Timestamp previousTxTimestamp = new Timestamp(sleepUntilMessageTimestamp);
|
||||
NextTransactionInfo nextTransactionInfo = this.repository.getATRepository().findNextTransaction(this.atData.getATAddress(),
|
||||
previousTxTimestamp.blockHeight,
|
||||
previousTxTimestamp.transactionSequence);
|
||||
|
||||
wakeDueToMessage = nextTransactionInfo != null;
|
||||
}
|
||||
|
||||
// Can we skip?
|
||||
if (!wakeDueToHeight && !wakeDueToMessage)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void preExecute(MachineState state) {
|
||||
// Sleep-until-message/height checking
|
||||
Long sleepUntilMessageTimestamp = this.atData.getSleepUntilMessageTimestamp();
|
||||
|
||||
if (sleepUntilMessageTimestamp != null) {
|
||||
// We've passed checks, so clear sleep-related flags/values
|
||||
this.setIsSleeping(state, false);
|
||||
this.setSleepUntilHeight(state, 0);
|
||||
this.atData.setSleepUntilMessageTimestamp(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Inherited methods from CIYAM AT API
|
||||
@@ -412,6 +450,10 @@ public class QortalATAPI extends API {
|
||||
|
||||
// Utility methods
|
||||
|
||||
public long calcFinalFees(MachineState state) {
|
||||
return state.getSteps() * this.ciyamAtSettings.feePerStep;
|
||||
}
|
||||
|
||||
/** Returns partial transaction signature, used to verify we're operating on the same transaction and not naively using block height & sequence. */
|
||||
public static byte[] partialSignature(byte[] fullSignature) {
|
||||
return Arrays.copyOfRange(fullSignature, 8, 32);
|
||||
@@ -460,6 +502,15 @@ public class QortalATAPI extends API {
|
||||
}
|
||||
}
|
||||
|
||||
/*package*/ void sleepUntilMessageOrHeight(MachineState state, long txTimestamp, Long sleepUntilHeight) {
|
||||
this.setIsSleeping(state, true);
|
||||
|
||||
this.atData.setSleepUntilMessageTimestamp(txTimestamp);
|
||||
|
||||
if (sleepUntilHeight != null)
|
||||
this.setSleepUntilHeight(state, sleepUntilHeight.intValue());
|
||||
}
|
||||
|
||||
/** Returns AT's account */
|
||||
/* package */ Account getATAccount() {
|
||||
return new Account(this.repository, this.atData.getATAddress());
|
||||
|
@@ -84,6 +84,43 @@ public enum QortalFunctionCode {
|
||||
api.setB(state, bBytes);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Sleep AT until a new message arrives after 'tx-timestamp'.<br>
|
||||
* <tt>0x0503 tx-timestamp</tt>
|
||||
*/
|
||||
SLEEP_UNTIL_MESSAGE(0x0503, 1, false) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
if (functionData.value1 <= 0)
|
||||
return;
|
||||
|
||||
long txTimestamp = functionData.value1;
|
||||
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
api.sleepUntilMessageOrHeight(state, txTimestamp, null);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Sleep AT until a new message arrives, after 'tx-timestamp', or height reached.<br>
|
||||
* <tt>0x0504 tx-timestamp height</tt>
|
||||
*/
|
||||
SLEEP_UNTIL_MESSAGE_OR_HEIGHT(0x0504, 2, false) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
if (functionData.value1 <= 0)
|
||||
return;
|
||||
|
||||
long txTimestamp = functionData.value1;
|
||||
|
||||
if (functionData.value2 <= 0)
|
||||
return;
|
||||
|
||||
long sleepUntilHeight = functionData.value2;
|
||||
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
api.sleepUntilMessageOrHeight(state, txTimestamp, sleepUntilHeight);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.<br>
|
||||
* <tt>0x0510</tt>
|
||||
|
@@ -1247,12 +1247,13 @@ public class Block {
|
||||
for (ATData atData : executableATs) {
|
||||
AT at = new AT(this.repository, atData);
|
||||
List<AtTransaction> atTransactions = at.run(this.blockData.getHeight(), this.blockData.getTimestamp());
|
||||
ATStateData atStateData = at.getATStateData();
|
||||
// Didn't execute? (e.g. sleeping)
|
||||
if (atStateData == null)
|
||||
continue;
|
||||
|
||||
allAtTransactions.addAll(atTransactions);
|
||||
|
||||
ATStateData atStateData = at.getATStateData();
|
||||
this.ourAtStates.add(atStateData);
|
||||
|
||||
this.ourAtFees += atStateData.getFees();
|
||||
}
|
||||
|
||||
|
@@ -406,14 +406,24 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) {
|
||||
long amount = 0;
|
||||
long total = 0L;
|
||||
long totalInputAmount = 0L;
|
||||
long totalOutputAmount = 0L;
|
||||
List<SimpleTransaction.Input> inputs = new ArrayList<>();
|
||||
List<SimpleTransaction.Output> outputs = new ArrayList<>();
|
||||
|
||||
for (BitcoinyTransaction.Input input : t.inputs) {
|
||||
try {
|
||||
BitcoinyTransaction t2 = getTransaction(input.outputTxHash);
|
||||
List<String> senders = t2.outputs.get(input.outputVout).addresses;
|
||||
long inputAmount = t2.outputs.get(input.outputVout).value;
|
||||
totalInputAmount += inputAmount;
|
||||
for (String sender : senders) {
|
||||
boolean addressInWallet = false;
|
||||
if (keySet.contains(sender)) {
|
||||
total += t2.outputs.get(input.outputVout).value;
|
||||
total += inputAmount;
|
||||
addressInWallet = true;
|
||||
}
|
||||
inputs.add(new SimpleTransaction.Input(sender, inputAmount, addressInWallet));
|
||||
}
|
||||
} catch (ForeignBlockchainException e) {
|
||||
LOGGER.trace("Failed to retrieve transaction information {}", input.outputTxHash);
|
||||
@@ -422,17 +432,22 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
if (t.outputs != null && !t.outputs.isEmpty()) {
|
||||
for (BitcoinyTransaction.Output output : t.outputs) {
|
||||
for (String address : output.addresses) {
|
||||
boolean addressInWallet = false;
|
||||
if (keySet.contains(address)) {
|
||||
if (total > 0L) {
|
||||
amount -= (total - output.value);
|
||||
} else {
|
||||
amount += output.value;
|
||||
}
|
||||
addressInWallet = true;
|
||||
}
|
||||
outputs.add(new SimpleTransaction.Output(address, output.value, addressInWallet));
|
||||
}
|
||||
totalOutputAmount += output.value;
|
||||
}
|
||||
}
|
||||
return new SimpleTransaction(t.txHash, t.timestamp, amount);
|
||||
long fee = totalInputAmount - totalOutputAmount;
|
||||
return new SimpleTransaction(t.txHash, t.timestamp, amount, fee, inputs, outputs);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -2,20 +2,85 @@ package org.qortal.crosschain;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import java.util.List;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class SimpleTransaction {
|
||||
private String txHash;
|
||||
private Integer timestamp;
|
||||
private long totalAmount;
|
||||
private long feeAmount;
|
||||
private List<Input> inputs;
|
||||
private List<Output> outputs;
|
||||
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class Input {
|
||||
private String address;
|
||||
private long amount;
|
||||
private boolean addressInWallet;
|
||||
|
||||
public Input() {
|
||||
}
|
||||
|
||||
public Input(String address, long amount, boolean addressInWallet) {
|
||||
this.address = address;
|
||||
this.amount = amount;
|
||||
this.addressInWallet = addressInWallet;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public long getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public boolean getAddressInWallet() {
|
||||
return addressInWallet;
|
||||
}
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public static class Output {
|
||||
private String address;
|
||||
private long amount;
|
||||
private boolean addressInWallet;
|
||||
|
||||
public Output() {
|
||||
}
|
||||
|
||||
public Output(String address, long amount, boolean addressInWallet) {
|
||||
this.address = address;
|
||||
this.amount = amount;
|
||||
this.addressInWallet = addressInWallet;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public long getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public boolean getAddressInWallet() {
|
||||
return addressInWallet;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public SimpleTransaction() {
|
||||
}
|
||||
|
||||
public SimpleTransaction(String txHash, Integer timestamp, long totalAmount) {
|
||||
public SimpleTransaction(String txHash, Integer timestamp, long totalAmount, long feeAmount, List<Input> inputs, List<Output> outputs) {
|
||||
this.txHash = txHash;
|
||||
this.timestamp = timestamp;
|
||||
this.totalAmount = totalAmount;
|
||||
this.feeAmount = feeAmount;
|
||||
this.inputs = inputs;
|
||||
this.outputs = outputs;
|
||||
}
|
||||
|
||||
public String getTxHash() {
|
||||
@@ -29,4 +94,16 @@ public class SimpleTransaction {
|
||||
public long getTotalAmount() {
|
||||
return totalAmount;
|
||||
}
|
||||
}
|
||||
|
||||
public long getFeeAmount() {
|
||||
return feeAmount;
|
||||
}
|
||||
|
||||
public List<Input> getInputs() {
|
||||
return this.inputs;
|
||||
}
|
||||
|
||||
public List<Output> getOutputs() {
|
||||
return this.outputs;
|
||||
}
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ public class ATData {
|
||||
private boolean isFrozen;
|
||||
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||
private Long frozenBalance;
|
||||
private Long sleepUntilMessageTimestamp;
|
||||
|
||||
// Constructors
|
||||
|
||||
@@ -31,7 +32,8 @@ public class ATData {
|
||||
}
|
||||
|
||||
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, byte[] codeHash,
|
||||
boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) {
|
||||
boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance,
|
||||
Long sleepUntilMessageTimestamp) {
|
||||
this.ATAddress = ATAddress;
|
||||
this.creatorPublicKey = creatorPublicKey;
|
||||
this.creation = creation;
|
||||
@@ -45,6 +47,7 @@ public class ATData {
|
||||
this.hadFatalError = hadFatalError;
|
||||
this.isFrozen = isFrozen;
|
||||
this.frozenBalance = frozenBalance;
|
||||
this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
|
||||
}
|
||||
|
||||
/** For constructing skeleton ATData with bare minimum info. */
|
||||
@@ -133,4 +136,12 @@ public class ATData {
|
||||
this.frozenBalance = frozenBalance;
|
||||
}
|
||||
|
||||
public Long getSleepUntilMessageTimestamp() {
|
||||
return this.sleepUntilMessageTimestamp;
|
||||
}
|
||||
|
||||
public void setSleepUntilMessageTimestamp(Long sleepUntilMessageTimestamp) {
|
||||
this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -10,35 +10,32 @@ public class ATStateData {
|
||||
private Long fees;
|
||||
private boolean isInitial;
|
||||
|
||||
// Qortal-AT-specific
|
||||
private Long sleepUntilMessageTimestamp;
|
||||
|
||||
// Constructors
|
||||
|
||||
/** Create new ATStateData */
|
||||
public ATStateData(String ATAddress, Integer height, byte[] stateData, byte[] stateHash, Long fees, boolean isInitial) {
|
||||
public ATStateData(String ATAddress, Integer height, byte[] stateData, byte[] stateHash, Long fees,
|
||||
boolean isInitial, Long sleepUntilMessageTimestamp) {
|
||||
this.ATAddress = ATAddress;
|
||||
this.height = height;
|
||||
this.stateData = stateData;
|
||||
this.stateHash = stateHash;
|
||||
this.fees = fees;
|
||||
this.isInitial = isInitial;
|
||||
this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
|
||||
}
|
||||
|
||||
/** For recreating per-block ATStateData from repository where not all info is needed */
|
||||
public ATStateData(String ATAddress, int height, byte[] stateHash, Long fees, boolean isInitial) {
|
||||
this(ATAddress, height, null, stateHash, fees, isInitial);
|
||||
}
|
||||
|
||||
/** For creating ATStateData from serialized bytes when we don't have all the info */
|
||||
public ATStateData(String ATAddress, byte[] stateHash) {
|
||||
// This won't ever be initial AT state from deployment as that's never serialized over the network,
|
||||
// but generated when the DeployAtTransaction is processed locally.
|
||||
this(ATAddress, null, null, stateHash, null, false);
|
||||
this(ATAddress, height, null, stateHash, fees, isInitial, null);
|
||||
}
|
||||
|
||||
/** For creating ATStateData from serialized bytes when we don't have all the info */
|
||||
public ATStateData(String ATAddress, byte[] stateHash, Long fees) {
|
||||
// This won't ever be initial AT state from deployment as that's never serialized over the network,
|
||||
// but generated when the DeployAtTransaction is processed locally.
|
||||
this(ATAddress, null, null, stateHash, fees, false);
|
||||
// This won't ever be initial AT state from deployment, as that's never serialized over the network.
|
||||
this(ATAddress, null, null, stateHash, fees, false, null);
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
@@ -72,4 +69,12 @@ public class ATStateData {
|
||||
return this.isInitial;
|
||||
}
|
||||
|
||||
public Long getSleepUntilMessageTimestamp() {
|
||||
return this.sleepUntilMessageTimestamp;
|
||||
}
|
||||
|
||||
public void setSleepUntilMessageTimestamp(Long sleepUntilMessageTimestamp) {
|
||||
this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
|
||||
}
|
||||
|
||||
}
|
||||
|
137
src/main/java/org/qortal/list/ResourceList.java
Normal file
137
src/main/java/org/qortal/list/ResourceList.java
Normal file
@@ -0,0 +1,137 @@
|
||||
package org.qortal.list;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.json.JSONArray;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ResourceList {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ResourceList.class);
|
||||
|
||||
private String category;
|
||||
private String resourceName;
|
||||
private List<String> list;
|
||||
|
||||
/**
|
||||
* ResourceList
|
||||
* Creates or updates a list for the purpose of tracking resources on the Qortal network
|
||||
* This can be used for local blocking, or even for curating and sharing content lists
|
||||
* Lists are backed off to JSON files (in the lists folder) to ease sharing between nodes and users
|
||||
*
|
||||
* @param category - for instance "blacklist", "whitelist", or "userlist"
|
||||
* @param resourceName - for instance "address", "poll", or "group"
|
||||
* @throws IOException
|
||||
*/
|
||||
public ResourceList(String category, String resourceName) throws IOException {
|
||||
this.category = category;
|
||||
this.resourceName = resourceName;
|
||||
this.load();
|
||||
}
|
||||
|
||||
|
||||
/* Filesystem */
|
||||
|
||||
private Path getFilePath() {
|
||||
String pathString = String.format("%s%s%s_%s.json", Settings.getInstance().getListsPath(),
|
||||
File.separator, this.resourceName, this.category);
|
||||
Path outputFilePath = Paths.get(pathString);
|
||||
try {
|
||||
Files.createDirectories(outputFilePath.getParent());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Unable to create lists directory");
|
||||
}
|
||||
return outputFilePath;
|
||||
}
|
||||
|
||||
public void save() throws IOException {
|
||||
if (this.resourceName == null) {
|
||||
throw new IllegalStateException("Can't save list with missing resource name");
|
||||
}
|
||||
if (this.category == null) {
|
||||
throw new IllegalStateException("Can't save list with missing category");
|
||||
}
|
||||
String jsonString = ResourceList.listToJSONString(this.list);
|
||||
|
||||
Path filePath = this.getFilePath();
|
||||
BufferedWriter writer = new BufferedWriter(new FileWriter(filePath.toString()));
|
||||
writer.write(jsonString);
|
||||
writer.close();
|
||||
}
|
||||
|
||||
private boolean load() throws IOException {
|
||||
Path path = this.getFilePath();
|
||||
File resourceListFile = new File(path.toString());
|
||||
if (!resourceListFile.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
String jsonString = new String(Files.readAllBytes(path));
|
||||
this.list = ResourceList.listFromJSONString(jsonString);
|
||||
} catch (IOException e) {
|
||||
throw new IOException(String.format("Couldn't read contents from file %s", path.toString()));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean revert() {
|
||||
try {
|
||||
return this.load();
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Unable to revert {} {}", this.resourceName, this.category);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/* List management */
|
||||
|
||||
public void add(String resource) {
|
||||
if (!this.contains(resource)) {
|
||||
this.list.add(resource);
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(String resource) {
|
||||
this.list.remove(resource);
|
||||
}
|
||||
|
||||
public boolean contains(String resource) {
|
||||
return this.list.contains(resource);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Utils */
|
||||
|
||||
public static String listToJSONString(List<String> list) {
|
||||
JSONArray items = new JSONArray();
|
||||
for (String item : list) {
|
||||
items.put(item);
|
||||
}
|
||||
return items.toString(4);
|
||||
}
|
||||
|
||||
private static List<String> listFromJSONString(String jsonString) {
|
||||
JSONArray jsonList = new JSONArray(jsonString);
|
||||
List<String> resourceList = new ArrayList<>();
|
||||
for (int i=0; i<jsonList.length(); i++) {
|
||||
String item = (String)jsonList.get(i);
|
||||
resourceList.add(item);
|
||||
}
|
||||
return resourceList;
|
||||
}
|
||||
|
||||
}
|
87
src/main/java/org/qortal/list/ResourceListManager.java
Normal file
87
src/main/java/org/qortal/list/ResourceListManager.java
Normal file
@@ -0,0 +1,87 @@
|
||||
package org.qortal.list;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ResourceListManager {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(ResourceListManager.class);
|
||||
|
||||
private static ResourceListManager instance;
|
||||
private ResourceList addressBlacklist;
|
||||
|
||||
public ResourceListManager() {
|
||||
try {
|
||||
this.addressBlacklist = new ResourceList("blacklist", "address");
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Error while loading address blacklist. Blocking is currently unavailable.");
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized ResourceListManager getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new ResourceListManager();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public boolean addAddressToBlacklist(String address, boolean save) {
|
||||
try {
|
||||
this.addressBlacklist.add(address);
|
||||
if (save) {
|
||||
this.addressBlacklist.save();
|
||||
}
|
||||
return true;
|
||||
|
||||
} catch (IllegalStateException | IOException e) {
|
||||
LOGGER.info("Unable to add address to blacklist", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removeAddressFromBlacklist(String address, boolean save) {
|
||||
try {
|
||||
this.addressBlacklist.remove(address);
|
||||
|
||||
if (save) {
|
||||
this.addressBlacklist.save();
|
||||
}
|
||||
return true;
|
||||
|
||||
} catch (IllegalStateException | IOException e) {
|
||||
LOGGER.info("Unable to remove address from blacklist", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAddressInBlacklist(String address) {
|
||||
if (this.addressBlacklist == null) {
|
||||
return false;
|
||||
}
|
||||
return this.addressBlacklist.contains(address);
|
||||
}
|
||||
|
||||
public void saveBlacklist() {
|
||||
if (this.addressBlacklist == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.addressBlacklist.save();
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Unable to save blacklist - reverting back to last saved state");
|
||||
this.addressBlacklist.revert();
|
||||
}
|
||||
}
|
||||
|
||||
public void revertBlacklist() {
|
||||
if (this.addressBlacklist == null) {
|
||||
return;
|
||||
}
|
||||
this.addressBlacklist.revert();
|
||||
}
|
||||
|
||||
}
|
@@ -103,7 +103,7 @@ public interface ATRepository {
|
||||
/**
|
||||
* Returns all ATStateData for a given block height.
|
||||
* <p>
|
||||
* Unlike <tt>getATState</tt>, only returns ATStateData saved at the given height.
|
||||
* Unlike <tt>getATState</tt>, only returns <i>partial</i> ATStateData saved at the given height.
|
||||
*
|
||||
* @param height
|
||||
* - block height
|
||||
|
@@ -32,7 +32,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
public ATData fromATAddress(String atAddress) throws DataException {
|
||||
String sql = "SELECT creator, created_when, version, asset_id, code_bytes, code_hash, "
|
||||
+ "is_sleeping, sleep_until_height, is_finished, had_fatal_error, "
|
||||
+ "is_frozen, frozen_balance "
|
||||
+ "is_frozen, frozen_balance, sleep_until_message_timestamp "
|
||||
+ "FROM ATs "
|
||||
+ "WHERE AT_address = ? LIMIT 1";
|
||||
|
||||
@@ -60,8 +60,13 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
if (frozenBalance == 0 && resultSet.wasNull())
|
||||
frozenBalance = null;
|
||||
|
||||
Long sleepUntilMessageTimestamp = resultSet.getLong(13);
|
||||
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
|
||||
sleepUntilMessageTimestamp = null;
|
||||
|
||||
return new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash,
|
||||
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
|
||||
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance,
|
||||
sleepUntilMessageTimestamp);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch AT from repository", e);
|
||||
}
|
||||
@@ -94,7 +99,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
public List<ATData> getAllExecutableATs() throws DataException {
|
||||
String sql = "SELECT AT_address, creator, created_when, version, asset_id, code_bytes, code_hash, "
|
||||
+ "is_sleeping, sleep_until_height, had_fatal_error, "
|
||||
+ "is_frozen, frozen_balance "
|
||||
+ "is_frozen, frozen_balance, sleep_until_message_timestamp "
|
||||
+ "FROM ATs "
|
||||
+ "WHERE is_finished = false "
|
||||
+ "ORDER BY created_when ASC";
|
||||
@@ -128,8 +133,13 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
if (frozenBalance == 0 && resultSet.wasNull())
|
||||
frozenBalance = null;
|
||||
|
||||
Long sleepUntilMessageTimestamp = resultSet.getLong(13);
|
||||
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
|
||||
sleepUntilMessageTimestamp = null;
|
||||
|
||||
ATData atData = new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash,
|
||||
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
|
||||
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance,
|
||||
sleepUntilMessageTimestamp);
|
||||
|
||||
executableATs.add(atData);
|
||||
} while (resultSet.next());
|
||||
@@ -147,7 +157,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
sql.append("SELECT AT_address, creator, created_when, version, asset_id, code_bytes, ")
|
||||
.append("is_sleeping, sleep_until_height, is_finished, had_fatal_error, ")
|
||||
.append("is_frozen, frozen_balance ")
|
||||
.append("is_frozen, frozen_balance, sleep_until_message_timestamp ")
|
||||
.append("FROM ATs ")
|
||||
.append("WHERE code_hash = ? ");
|
||||
bindParams.add(codeHash);
|
||||
@@ -191,8 +201,13 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
if (frozenBalance == 0 && resultSet.wasNull())
|
||||
frozenBalance = null;
|
||||
|
||||
Long sleepUntilMessageTimestamp = resultSet.getLong(13);
|
||||
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
|
||||
sleepUntilMessageTimestamp = null;
|
||||
|
||||
ATData atData = new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash,
|
||||
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
|
||||
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance,
|
||||
sleepUntilMessageTimestamp);
|
||||
|
||||
matchingATs.add(atData);
|
||||
} while (resultSet.next());
|
||||
@@ -210,7 +225,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
sql.append("SELECT AT_address, creator, created_when, version, asset_id, code_bytes, ")
|
||||
.append("is_sleeping, sleep_until_height, is_finished, had_fatal_error, ")
|
||||
.append("is_frozen, frozen_balance, code_hash ")
|
||||
.append("is_frozen, frozen_balance, code_hash, sleep_until_message_timestamp ")
|
||||
.append("FROM ");
|
||||
|
||||
// (VALUES (?), (?), ...) AS ATCodeHashes (code_hash)
|
||||
@@ -264,9 +279,10 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
frozenBalance = null;
|
||||
|
||||
byte[] codeHash = resultSet.getBytes(13);
|
||||
Long sleepUntilMessageTimestamp = resultSet.getLong(14);
|
||||
|
||||
ATData atData = new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash,
|
||||
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
|
||||
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance, sleepUntilMessageTimestamp);
|
||||
|
||||
matchingATs.add(atData);
|
||||
} while (resultSet.next());
|
||||
@@ -305,7 +321,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
.bind("code_bytes", atData.getCodeBytes()).bind("code_hash", atData.getCodeHash())
|
||||
.bind("is_sleeping", atData.getIsSleeping()).bind("sleep_until_height", atData.getSleepUntilHeight())
|
||||
.bind("is_finished", atData.getIsFinished()).bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen())
|
||||
.bind("frozen_balance", atData.getFrozenBalance());
|
||||
.bind("frozen_balance", atData.getFrozenBalance()).bind("sleep_until_message_timestamp", atData.getSleepUntilMessageTimestamp());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
@@ -328,7 +344,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
@Override
|
||||
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException {
|
||||
String sql = "SELECT state_data, state_hash, fees, is_initial "
|
||||
String sql = "SELECT state_data, state_hash, fees, is_initial, sleep_until_message_timestamp "
|
||||
+ "FROM ATStates "
|
||||
+ "LEFT OUTER JOIN ATStatesData USING (AT_address, height) "
|
||||
+ "WHERE ATStates.AT_address = ? AND ATStates.height = ? "
|
||||
@@ -343,7 +359,11 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
long fees = resultSet.getLong(3);
|
||||
boolean isInitial = resultSet.getBoolean(4);
|
||||
|
||||
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
|
||||
Long sleepUntilMessageTimestamp = resultSet.getLong(5);
|
||||
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
|
||||
sleepUntilMessageTimestamp = null;
|
||||
|
||||
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial, sleepUntilMessageTimestamp);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch AT state from repository", e);
|
||||
}
|
||||
@@ -351,7 +371,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
@Override
|
||||
public ATStateData getLatestATState(String atAddress) throws DataException {
|
||||
String sql = "SELECT height, state_data, state_hash, fees, is_initial "
|
||||
String sql = "SELECT height, state_data, state_hash, fees, is_initial, sleep_until_message_timestamp "
|
||||
+ "FROM ATStates "
|
||||
+ "JOIN ATStatesData USING (AT_address, height) "
|
||||
+ "WHERE ATStates.AT_address = ? "
|
||||
@@ -370,7 +390,11 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
long fees = resultSet.getLong(4);
|
||||
boolean isInitial = resultSet.getBoolean(5);
|
||||
|
||||
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
|
||||
Long sleepUntilMessageTimestamp = resultSet.getLong(6);
|
||||
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
|
||||
sleepUntilMessageTimestamp = null;
|
||||
|
||||
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial, sleepUntilMessageTimestamp);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch latest AT state from repository", e);
|
||||
}
|
||||
@@ -383,10 +407,10 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial "
|
||||
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial, FinalATStates.sleep_until_message_timestamp "
|
||||
+ "FROM ATs "
|
||||
+ "CROSS JOIN LATERAL("
|
||||
+ "SELECT height, state_data, state_hash, fees, is_initial "
|
||||
+ "SELECT height, state_data, state_hash, fees, is_initial, sleep_until_message_timestamp "
|
||||
+ "FROM ATStates "
|
||||
+ "JOIN ATStatesData USING (AT_address, height) "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address ");
|
||||
@@ -440,7 +464,11 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
long fees = resultSet.getLong(5);
|
||||
boolean isInitial = resultSet.getBoolean(6);
|
||||
|
||||
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
|
||||
Long sleepUntilMessageTimestamp = resultSet.getLong(7);
|
||||
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
|
||||
sleepUntilMessageTimestamp = null;
|
||||
|
||||
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial, sleepUntilMessageTimestamp);
|
||||
|
||||
atStates.add(atStateData);
|
||||
} while (resultSet.next());
|
||||
@@ -471,7 +499,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
|
||||
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial "
|
||||
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial, sleep_until_message_timestamp "
|
||||
+ "FROM ATs "
|
||||
+ "CROSS JOIN LATERAL("
|
||||
+ "SELECT height, state_data, state_hash, fees, is_initial "
|
||||
@@ -526,8 +554,10 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
byte[] stateHash = resultSet.getBytes(4);
|
||||
long fees = resultSet.getLong(5);
|
||||
boolean isInitial = resultSet.getBoolean(6);
|
||||
Long sleepUntilMessageTimestamp = resultSet.getLong(7);
|
||||
|
||||
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
|
||||
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial,
|
||||
sleepUntilMessageTimestamp);
|
||||
|
||||
atStates.add(atStateData);
|
||||
} while (resultSet.next());
|
||||
@@ -662,7 +692,8 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
atStatesSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
|
||||
.bind("state_hash", atStateData.getStateHash())
|
||||
.bind("fees", atStateData.getFees()).bind("is_initial", atStateData.isInitial());
|
||||
.bind("fees", atStateData.getFees()).bind("is_initial", atStateData.isInitial())
|
||||
.bind("sleep_until_message_timestamp", atStateData.getSleepUntilMessageTimestamp());
|
||||
|
||||
try {
|
||||
atStatesSaver.execute(this.repository);
|
||||
|
@@ -699,7 +699,7 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("CHECKPOINT");
|
||||
break;
|
||||
|
||||
case 30:
|
||||
case 30: {
|
||||
// Split AT state data off to new table for better performance/management.
|
||||
|
||||
if (!wasPristine && !"mem".equals(HSQLDBRepository.getDbPathname(connection.getMetaData().getURL()))) {
|
||||
@@ -774,6 +774,7 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("ALTER TABLE ATStatesNew RENAME TO ATStates");
|
||||
stmt.execute("CHECKPOINT");
|
||||
break;
|
||||
}
|
||||
|
||||
case 31:
|
||||
// Fix latest AT state cache which was previous created as TEMPORARY
|
||||
@@ -822,6 +823,41 @@ public class HSQLDBDatabaseUpdates {
|
||||
+ "timestamp_signature Signature NOT NULL, " + TRANSACTION_KEYS + ")");
|
||||
break;
|
||||
|
||||
case 34: {
|
||||
// AT sleep-until-message support
|
||||
LOGGER.info("Altering AT table in repository - this might take a while... (approx. 20 seconds on high-spec)");
|
||||
stmt.execute("ALTER TABLE ATs ADD sleep_until_message_timestamp BIGINT");
|
||||
|
||||
// Create new AT-states table with new column
|
||||
stmt.execute("CREATE TABLE ATStatesNew ("
|
||||
+ "AT_address QortalAddress, height INTEGER NOT NULL, state_hash ATStateHash NOT NULL, "
|
||||
+ "fees QortalAmount NOT NULL, is_initial BOOLEAN NOT NULL, sleep_until_message_timestamp BIGINT, "
|
||||
+ "PRIMARY KEY (AT_address, height), "
|
||||
+ "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
|
||||
stmt.execute("SET TABLE ATStatesNew NEW SPACE");
|
||||
stmt.execute("CHECKPOINT");
|
||||
|
||||
ResultSet resultSet = stmt.executeQuery("SELECT height FROM Blocks ORDER BY height DESC LIMIT 1");
|
||||
final int blockchainHeight = resultSet.next() ? resultSet.getInt(1) : 0;
|
||||
final int heightStep = 100;
|
||||
|
||||
LOGGER.info("Altering AT states table in repository - this might take a while... (approx. 3 mins on high-spec)");
|
||||
for (int minHeight = 1; minHeight < blockchainHeight; minHeight += heightStep) {
|
||||
stmt.execute("INSERT INTO ATStatesNew ("
|
||||
+ "SELECT AT_address, height, state_hash, fees, is_initial, NULL "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE height BETWEEN " + minHeight + " AND " + (minHeight + heightStep - 1)
|
||||
+ ")");
|
||||
stmt.execute("COMMIT");
|
||||
}
|
||||
stmt.execute("CHECKPOINT");
|
||||
|
||||
stmt.execute("DROP TABLE ATStates");
|
||||
stmt.execute("ALTER TABLE ATStatesNew RENAME TO ATStates");
|
||||
stmt.execute("CHECKPOINT");
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@@ -161,6 +161,9 @@ public class Settings {
|
||||
"https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update"
|
||||
};
|
||||
|
||||
// Lists
|
||||
private String listsPath = "lists";
|
||||
|
||||
/** Array of NTP server hostnames. */
|
||||
private String[] ntpServers = new String[] {
|
||||
"pool.ntp.org",
|
||||
@@ -474,6 +477,10 @@ public class Settings {
|
||||
return this.autoUpdateRepos;
|
||||
}
|
||||
|
||||
public String getListsPath() {
|
||||
return this.listsPath;
|
||||
}
|
||||
|
||||
public String[] getNtpServers() {
|
||||
return this.ntpServers;
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.data.transaction.ChatTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.list.ResourceListManager;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.GroupRepository;
|
||||
import org.qortal.repository.Repository;
|
||||
@@ -138,6 +139,12 @@ public class ChatTransaction extends Transaction {
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Nonce checking is done via isSignatureValid() as that method is only called once per import
|
||||
|
||||
// Check for blacklisted author by address
|
||||
ResourceListManager listManager = ResourceListManager.getInstance();
|
||||
if (listManager.isAddressInBlacklist(this.chatTransactionData.getSender())) {
|
||||
return ValidationResult.ADDRESS_IN_BLACKLIST;
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (this.repository.getTransactionRepository().exists(this.chatTransactionData.getSignature()))
|
||||
|
@@ -247,6 +247,7 @@ public abstract class Transaction {
|
||||
INVALID_GROUP_BLOCK_DELAY(93),
|
||||
INCORRECT_NONCE(94),
|
||||
INVALID_TIMESTAMP_SIGNATURE(95),
|
||||
ADDRESS_IN_BLACKLIST(96),
|
||||
INVALID_BUT_OK(999),
|
||||
NOT_YET_RELEASED(1000);
|
||||
|
||||
|
@@ -354,7 +354,8 @@ public class AtRepositoryTests extends Common {
|
||||
/*StateData*/ null,
|
||||
atStateData.getStateHash(),
|
||||
atStateData.getFees(),
|
||||
atStateData.isInitial());
|
||||
atStateData.isInitial(),
|
||||
atStateData.getSleepUntilMessageTimestamp());
|
||||
repository.getATRepository().save(newAtStateData);
|
||||
|
||||
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);
|
||||
|
@@ -0,0 +1,365 @@
|
||||
package org.qortal.test.at;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.ciyam.at.CompilationException;
|
||||
import org.ciyam.at.FunctionCode;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.ciyam.at.Timestamp;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
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.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
public class SleepUntilMessageOrHeightTests extends Common {
|
||||
|
||||
private static final byte[] messageData = new byte[] { 0x44 };
|
||||
private static final byte[] creationBytes = buildSleepUntilMessageOrHeightAT();
|
||||
private static final long fundingAmount = 1_00000000L;
|
||||
private static final long WAKE_HEIGHT = 10L;
|
||||
|
||||
private Repository repository = null;
|
||||
private PrivateKeyAccount deployer;
|
||||
private DeployAtTransaction deployAtTransaction;
|
||||
private Account atAccount;
|
||||
private String atAddress;
|
||||
private byte[] rawNextTimestamp = new byte[32];
|
||||
private Transaction transaction;
|
||||
|
||||
@Before
|
||||
public void before() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
|
||||
this.repository = RepositoryManager.getRepository();
|
||||
this.deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
this.deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
this.atAccount = deployAtTransaction.getATAccount();
|
||||
this.atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() throws DataException {
|
||||
if (this.repository != null)
|
||||
this.repository.close();
|
||||
|
||||
this.repository = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeploy() throws DataException {
|
||||
// Confirm initial value is zero
|
||||
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
|
||||
assertArrayEquals(new byte[32], rawNextTimestamp);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFeelessSleep() throws DataException {
|
||||
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Fetch AT's balance for this height
|
||||
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
// Mint block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Fetch new AT balance
|
||||
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals(preMintBalance, postMintBalance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFeelessSleep2() throws DataException {
|
||||
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Fetch AT's balance for this height
|
||||
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
// Mint several blocks
|
||||
for (int i = 0; i < 5; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Fetch new AT balance
|
||||
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals(preMintBalance, postMintBalance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSleepUntilMessage() throws DataException {
|
||||
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE_OR_HEIGHT
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send message to AT
|
||||
transaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Mint block so AT executes and finds message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Confirm AT finds message
|
||||
assertTimestamp(repository, atAddress, transaction);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSleepUntilHeight() throws DataException {
|
||||
// AT deployment in block 2
|
||||
|
||||
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE_OR_HEIGHT
|
||||
BlockUtils.mintBlock(repository); // height now 3
|
||||
|
||||
// Fetch AT's balance for this height
|
||||
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
// Mint several blocks
|
||||
for (int i = 3; i < WAKE_HEIGHT; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// We should now be at WAKE_HEIGHT
|
||||
long height = repository.getBlockRepository().getBlockchainHeight();
|
||||
assertEquals(WAKE_HEIGHT, height);
|
||||
|
||||
// AT should have woken and run at this height so balance should have changed
|
||||
|
||||
// Fetch new AT balance
|
||||
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertNotSame(preMintBalance, postMintBalance);
|
||||
|
||||
// Confirm AT has no message
|
||||
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
|
||||
assertArrayEquals(new byte[32], rawNextTimestamp);
|
||||
|
||||
// Mint yet another block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// AT should also have woken and run at this height so balance should have changed
|
||||
|
||||
// Fetch new AT balance
|
||||
long postMint2Balance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertNotSame(postMintBalance, postMint2Balance);
|
||||
|
||||
// Confirm AT still has no message
|
||||
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
|
||||
assertArrayEquals(new byte[32], rawNextTimestamp);
|
||||
|
||||
}
|
||||
|
||||
private static byte[] buildSleepUntilMessageOrHeightAT() {
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0;
|
||||
|
||||
// Beginning of data segment for easy extraction
|
||||
final int addrNextTx = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrNextTxIndex = addrCounter++;
|
||||
|
||||
final int addrLastTxTimestamp = addrCounter++;
|
||||
|
||||
final int addrWakeHeight = addrCounter++;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
// skip addrNextTx
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 4 * MachineState.VALUE_SIZE);
|
||||
|
||||
// Store pointer to addrNextTx at addrNextTxIndex
|
||||
dataByteBuffer.putLong(addrNextTx);
|
||||
|
||||
// skip addrLastTxTimestamp
|
||||
dataByteBuffer.position(dataByteBuffer.position() + MachineState.VALUE_SIZE);
|
||||
|
||||
// Store fixed wake height (block 10)
|
||||
dataByteBuffer.putLong(WAKE_HEIGHT);
|
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
codeByteBuffer.clear();
|
||||
|
||||
try {
|
||||
/* Initialization */
|
||||
|
||||
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
|
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||
|
||||
/* Loop, waiting for message to AT */
|
||||
|
||||
/* Sleep until message arrives */
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT_2.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE_OR_HEIGHT.value, addrLastTxTimestamp, addrWakeHeight));
|
||||
|
||||
// Find next transaction to this AT since the last one (if any)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp));
|
||||
|
||||
// Copy A to data segment, starting at addrNextTx (as pointed to by addrNextTxIndex)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_A_IND, addrNextTxIndex));
|
||||
|
||||
// Stop if timestamp part of A is zero
|
||||
codeByteBuffer.put(OpCode.STZ_DAT.compile(addrNextTx));
|
||||
|
||||
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxTimestamp));
|
||||
|
||||
// We're done
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
||||
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile AT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
final short ciyamAtVersion = 2;
|
||||
final short numCallStackPages = 0;
|
||||
final short numUserStackPages = 0;
|
||||
final long minActivationAmount = 0L;
|
||||
|
||||
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = deployer.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
String name = "Test AT";
|
||||
String description = "Test AT";
|
||||
String atType = "Test";
|
||||
String tags = "TEST";
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
|
||||
|
||||
return deployAtTransaction;
|
||||
}
|
||||
|
||||
private void extractNextTxTimestamp(Repository repository, String atAddress, byte[] rawNextTimestamp) throws DataException {
|
||||
// Check AT result
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
byte[] dataBytes = MachineState.extractDataBytes(stateData);
|
||||
|
||||
System.arraycopy(dataBytes, 0, rawNextTimestamp, 0, rawNextTimestamp.length);
|
||||
}
|
||||
|
||||
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = sender.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
int version = 4;
|
||||
int nonce = 0;
|
||||
long amount = 0;
|
||||
Long assetId = null; // because amount is zero
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
|
||||
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
|
||||
|
||||
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
|
||||
|
||||
fee = messageTransaction.calcRecommendedFee();
|
||||
messageTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndImportValid(repository, messageTransactionData, sender);
|
||||
|
||||
return messageTransaction;
|
||||
}
|
||||
|
||||
private void assertTimestamp(Repository repository, String atAddress, Transaction transaction) throws DataException {
|
||||
int height = transaction.getHeight();
|
||||
byte[] transactionSignature = transaction.getTransactionData().getSignature();
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
assertNotNull(blockData);
|
||||
|
||||
Block block = new Block(repository, blockData);
|
||||
|
||||
List<Transaction> blockTransactions = block.getTransactions();
|
||||
int sequence;
|
||||
for (sequence = blockTransactions.size() - 1; sequence >= 0; --sequence)
|
||||
if (Arrays.equals(blockTransactions.get(sequence).getTransactionData().getSignature(), transactionSignature))
|
||||
break;
|
||||
|
||||
assertNotSame(-1, sequence);
|
||||
|
||||
byte[] rawNextTimestamp = new byte[32];
|
||||
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
|
||||
|
||||
Timestamp expectedTimestamp = new Timestamp(height, sequence);
|
||||
Timestamp actualTimestamp = new Timestamp(BitTwiddling.longFromBEBytes(rawNextTimestamp, 0));
|
||||
|
||||
assertEquals(String.format("Expected height %d, seq %d but was height %d, seq %d",
|
||||
height, sequence,
|
||||
actualTimestamp.blockHeight, actualTimestamp.transactionSequence
|
||||
),
|
||||
expectedTimestamp.longValue(),
|
||||
actualTimestamp.longValue());
|
||||
|
||||
byte[] expectedPartialSignature = new byte[24];
|
||||
System.arraycopy(transactionSignature, 8, expectedPartialSignature, 0, expectedPartialSignature.length);
|
||||
|
||||
byte[] actualPartialSignature = new byte[24];
|
||||
System.arraycopy(rawNextTimestamp, 8, actualPartialSignature, 0, actualPartialSignature.length);
|
||||
|
||||
assertArrayEquals(expectedPartialSignature, actualPartialSignature);
|
||||
}
|
||||
|
||||
}
|
311
src/test/java/org/qortal/test/at/SleepUntilMessageTests.java
Normal file
311
src/test/java/org/qortal/test/at/SleepUntilMessageTests.java
Normal file
@@ -0,0 +1,311 @@
|
||||
package org.qortal.test.at;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.ciyam.at.CompilationException;
|
||||
import org.ciyam.at.FunctionCode;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.OpCode;
|
||||
import org.ciyam.at.Timestamp;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.at.QortalFunctionCode;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
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.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.MessageTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.BitTwiddling;
|
||||
|
||||
public class SleepUntilMessageTests extends Common {
|
||||
|
||||
private static final byte[] messageData = new byte[] { 0x44 };
|
||||
private static final byte[] creationBytes = buildSleepUntilMessageAT();
|
||||
private static final long fundingAmount = 1_00000000L;
|
||||
|
||||
private Repository repository = null;
|
||||
private PrivateKeyAccount deployer;
|
||||
private DeployAtTransaction deployAtTransaction;
|
||||
private Account atAccount;
|
||||
private String atAddress;
|
||||
private byte[] rawNextTimestamp = new byte[32];
|
||||
private Transaction transaction;
|
||||
|
||||
@Before
|
||||
public void before() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
|
||||
this.repository = RepositoryManager.getRepository();
|
||||
this.deployer = Common.getTestAccount(repository, "alice");
|
||||
|
||||
this.deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
|
||||
this.atAccount = deployAtTransaction.getATAccount();
|
||||
this.atAddress = deployAtTransaction.getATAccount().getAddress();
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() throws DataException {
|
||||
if (this.repository != null)
|
||||
this.repository.close();
|
||||
|
||||
this.repository = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeploy() throws DataException {
|
||||
// Confirm initial value is zero
|
||||
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
|
||||
assertArrayEquals(new byte[32], rawNextTimestamp);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFeelessSleep() throws DataException {
|
||||
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Fetch AT's balance for this height
|
||||
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
// Mint block
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Fetch new AT balance
|
||||
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals(preMintBalance, postMintBalance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFeelessSleep2() throws DataException {
|
||||
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Fetch AT's balance for this height
|
||||
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
// Mint several blocks
|
||||
for (int i = 0; i < 10; ++i)
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Fetch new AT balance
|
||||
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
|
||||
|
||||
assertEquals(preMintBalance, postMintBalance);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSleepUntilMessage() throws DataException {
|
||||
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Send message to AT
|
||||
transaction = sendMessage(repository, deployer, messageData, atAddress);
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Mint block so AT executes and finds message
|
||||
BlockUtils.mintBlock(repository);
|
||||
|
||||
// Confirm AT finds message
|
||||
assertTimestamp(repository, atAddress, transaction);
|
||||
}
|
||||
|
||||
private static byte[] buildSleepUntilMessageAT() {
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0;
|
||||
|
||||
// Beginning of data segment for easy extraction
|
||||
final int addrNextTx = addrCounter;
|
||||
addrCounter += 4;
|
||||
|
||||
final int addrNextTxIndex = addrCounter++;
|
||||
|
||||
final int addrLastTxTimestamp = addrCounter++;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
|
||||
|
||||
// skip addrNextTx
|
||||
dataByteBuffer.position(dataByteBuffer.position() + 4 * MachineState.VALUE_SIZE);
|
||||
|
||||
// Store pointer to addrNextTx at addrNextTxIndex
|
||||
dataByteBuffer.putLong(addrNextTx);
|
||||
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
|
||||
|
||||
// Two-pass version
|
||||
for (int pass = 0; pass < 2; ++pass) {
|
||||
codeByteBuffer.clear();
|
||||
|
||||
try {
|
||||
/* Initialization */
|
||||
|
||||
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp));
|
||||
|
||||
// Set restart position to after this opcode
|
||||
codeByteBuffer.put(OpCode.SET_PCS.compile());
|
||||
|
||||
/* Loop, waiting for message to AT */
|
||||
|
||||
/* Sleep until message arrives */
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxTimestamp));
|
||||
|
||||
// Find next transaction to this AT since the last one (if any)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp));
|
||||
|
||||
// Copy A to data segment, starting at addrNextTx (as pointed to by addrNextTxIndex)
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_A_IND, addrNextTxIndex));
|
||||
|
||||
// Stop if timestamp part of A is zero
|
||||
codeByteBuffer.put(OpCode.STZ_DAT.compile(addrNextTx));
|
||||
|
||||
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxTimestamp));
|
||||
|
||||
// We're done
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.compile());
|
||||
|
||||
} catch (CompilationException e) {
|
||||
throw new IllegalStateException("Unable to compile AT?", e);
|
||||
}
|
||||
}
|
||||
|
||||
codeByteBuffer.flip();
|
||||
|
||||
byte[] codeBytes = new byte[codeByteBuffer.limit()];
|
||||
codeByteBuffer.get(codeBytes);
|
||||
|
||||
final short ciyamAtVersion = 2;
|
||||
final short numCallStackPages = 0;
|
||||
final short numUserStackPages = 0;
|
||||
final long minActivationAmount = 0L;
|
||||
|
||||
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
|
||||
}
|
||||
|
||||
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = deployer.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
String name = "Test AT";
|
||||
String description = "Test AT";
|
||||
String atType = "Test";
|
||||
String tags = "TEST";
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
|
||||
|
||||
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
|
||||
|
||||
return deployAtTransaction;
|
||||
}
|
||||
|
||||
private void extractNextTxTimestamp(Repository repository, String atAddress, byte[] rawNextTimestamp) throws DataException {
|
||||
// Check AT result
|
||||
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
|
||||
byte[] stateData = atStateData.getStateData();
|
||||
|
||||
byte[] dataBytes = MachineState.extractDataBytes(stateData);
|
||||
|
||||
System.arraycopy(dataBytes, 0, rawNextTimestamp, 0, rawNextTimestamp.length);
|
||||
}
|
||||
|
||||
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = sender.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Long fee = null;
|
||||
int version = 4;
|
||||
int nonce = 0;
|
||||
long amount = 0;
|
||||
Long assetId = null; // because amount is zero
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
|
||||
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
|
||||
|
||||
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
|
||||
|
||||
fee = messageTransaction.calcRecommendedFee();
|
||||
messageTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndImportValid(repository, messageTransactionData, sender);
|
||||
|
||||
return messageTransaction;
|
||||
}
|
||||
|
||||
private void assertTimestamp(Repository repository, String atAddress, Transaction transaction) throws DataException {
|
||||
int height = transaction.getHeight();
|
||||
byte[] transactionSignature = transaction.getTransactionData().getSignature();
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(height);
|
||||
assertNotNull(blockData);
|
||||
|
||||
Block block = new Block(repository, blockData);
|
||||
|
||||
List<Transaction> blockTransactions = block.getTransactions();
|
||||
int sequence;
|
||||
for (sequence = blockTransactions.size() - 1; sequence >= 0; --sequence)
|
||||
if (Arrays.equals(blockTransactions.get(sequence).getTransactionData().getSignature(), transactionSignature))
|
||||
break;
|
||||
|
||||
assertNotSame(-1, sequence);
|
||||
|
||||
byte[] rawNextTimestamp = new byte[32];
|
||||
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
|
||||
|
||||
Timestamp expectedTimestamp = new Timestamp(height, sequence);
|
||||
Timestamp actualTimestamp = new Timestamp(BitTwiddling.longFromBEBytes(rawNextTimestamp, 0));
|
||||
|
||||
assertEquals(String.format("Expected height %d, seq %d but was height %d, seq %d",
|
||||
height, sequence,
|
||||
actualTimestamp.blockHeight, actualTimestamp.transactionSequence
|
||||
),
|
||||
expectedTimestamp.longValue(),
|
||||
actualTimestamp.longValue());
|
||||
|
||||
byte[] expectedPartialSignature = new byte[24];
|
||||
System.arraycopy(transactionSignature, 8, expectedPartialSignature, 0, expectedPartialSignature.length);
|
||||
|
||||
byte[] actualPartialSignature = new byte[24];
|
||||
System.arraycopy(rawNextTimestamp, 8, actualPartialSignature, 0, actualPartialSignature.length);
|
||||
|
||||
assertArrayEquals(expectedPartialSignature, actualPartialSignature);
|
||||
}
|
||||
|
||||
}
|
@@ -35,10 +35,10 @@ public class DogecoinTests extends Common {
|
||||
@Test
|
||||
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
|
||||
long before = System.currentTimeMillis();
|
||||
System.out.println(String.format("Bitcoin median blocktime: %d", dogecoin.getMedianBlockTime()));
|
||||
System.out.println(String.format("Dogecoin median blocktime: %d", dogecoin.getMedianBlockTime()));
|
||||
long afterFirst = System.currentTimeMillis();
|
||||
|
||||
System.out.println(String.format("Bitcoin median blocktime: %d", dogecoin.getMedianBlockTime()));
|
||||
System.out.println(String.format("Dogecoin median blocktime: %d", dogecoin.getMedianBlockTime()));
|
||||
long afterSecond = System.currentTimeMillis();
|
||||
|
||||
long firstPeriod = afterFirst - before;
|
||||
@@ -64,10 +64,11 @@ public class DogecoinTests extends Common {
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
|
||||
public void testBuildSpend() {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
|
||||
|
||||
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||
String recipient = "DP1iFao33xdEPa5vaArpj7sykfzKNeiJeX";
|
||||
long amount = 1000L;
|
||||
|
||||
Transaction transaction = dogecoin.buildSpend(xprv58, recipient, amount);
|
||||
@@ -81,7 +82,7 @@ public class DogecoinTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testGetWalletBalance() {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
|
||||
|
||||
Long balance = dogecoin.getWalletBalance(xprv58);
|
||||
|
||||
@@ -102,7 +103,7 @@ public class DogecoinTests extends Common {
|
||||
|
||||
@Test
|
||||
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
|
||||
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
|
||||
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
|
||||
|
||||
String address = dogecoin.getUnusedReceiveAddress(xprv58);
|
||||
|
||||
|
Reference in New Issue
Block a user