Loads of work on CIYAM AT support, including BTC-QORT cross-chain trading.

We require AT v1.3.4 now!

Updated AT-related logging.

Added "isInitial" flag to AT state data so that state data created at
deployment is not added to serialized block data.

Updated BTC-QORT AT code and tests to cover various scenarios.

Added missing 'testNtpOffset' to various test versions of 'settings.json'.
Added missing 'ciyamAtSettings' to various test blockchain configs.

Loads of AT-related additions/fixes/etc. to core code, e.g Block
This commit is contained in:
catbref 2020-04-14 17:19:44 +01:00
parent 3eaeb927ec
commit 98506a038b
38 changed files with 3144 additions and 266 deletions

View File

@ -9,6 +9,7 @@
<bitcoinj.version>0.15.5</bitcoinj.version>
<bouncycastle.version>1.64</bouncycastle.version>
<build.timestamp>${maven.build.timestamp}</build.timestamp>
<ciyam-at.version>1.3.4</ciyam-at.version>
<commons-net.version>3.6</commons-net.version>
<commons-text.version>1.8</commons-text.version>
<dagger.version>1.2.2</dagger.version>
@ -405,8 +406,8 @@
<!-- CIYAM AT (automated transactions) -->
<dependency>
<groupId>org.ciyam</groupId>
<artifactId>at</artifactId>
<version>1.3.2</version>
<artifactId>AT</artifactId>
<version>${ciyam-at.version}</version>
</dependency>
<!-- Bitcoin support -->
<dependency>

View File

@ -54,17 +54,18 @@ public class AT {
long blockTimestamp = Timestamp.toLong(height, 0);
QortalATAPI api = new QortalATAPI(repository, skeletonAtData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
MachineState machineState = new MachineState(api, deployATTransactionData.getCreationBytes());
MachineState machineState = new MachineState(api, loggerFactory, deployATTransactionData.getCreationBytes());
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(),
machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(),
machineState.getIsFrozen(), machineState.getFrozenBalance());
machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
machineState.isFrozen(), machineState.getFrozenBalance());
byte[] stateData = machineState.toBytes();
byte[] stateHash = Crypto.digest(stateData);
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, BigDecimal.ZERO.setScale(8));
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, BigDecimal.ZERO.setScale(8), true);
} else {
// Legacy v1 AT
// We would deploy these in 'dead' state as they will never be run on Qortal
@ -107,7 +108,7 @@ public class AT {
this.atData = new ATData(atAddress, creatorPublicKey, creation, version, Asset.QORT, codeBytes, isSleeping, sleepUntilHeight, isFinished,
hadFatalError, isFrozen, frozenBalance);
this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8));
this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8), true);
}
}
@ -133,34 +134,87 @@ public class AT {
this.repository.getATRepository().delete(this.atData.getATAddress());
}
public List<AtTransaction> run(long blockTimestamp) 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);
QortalATLogger logger = new QortalATLogger();
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] codeBytes = this.atData.getCodeBytes();
// Fetch latest ATStateData for this AT (if any)
// Fetch latest ATStateData for this AT
ATStateData latestAtStateData = this.repository.getATRepository().getLatestATState(atAddress);
// There should be at least initial AT state data
// There should be at least initial deployment AT state data
if (latestAtStateData == null)
throw new IllegalStateException("No initial AT state data found");
throw new IllegalStateException("No previous AT state data found");
// [Re]create AT machine state using AT state data or from scratch as applicable
MachineState state = MachineState.fromBytes(api, logger, latestAtStateData.getStateData(), codeBytes);
MachineState state = MachineState.fromBytes(api, loggerFactory, latestAtStateData.getStateData(), codeBytes);
state.execute();
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
long creation = this.atData.getCreation();
byte[] stateData = state.toBytes();
byte[] stateHash = Crypto.digest(stateData);
BigDecimal atFees = api.calcFinalFees(state);
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, atFees);
this.atStateData = new ATStateData(atAddress, blockHeight, creation, stateData, stateHash, atFees, false);
return api.getTransactions();
}
public void update(int blockHeight, long blockTimestamp) throws DataException {
// [Re]create AT machine state using AT state data or from scratch as applicable
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] codeBytes = this.atData.getCodeBytes();
MachineState state = MachineState.fromBytes(api, loggerFactory, this.atStateData.getStateData(), codeBytes);
// Save latest AT state data
this.repository.getATRepository().save(this.atStateData);
// Update AT info in repository too
this.atData.setIsSleeping(state.isSleeping());
this.atData.setSleepUntilHeight(state.getSleepUntilHeight());
this.atData.setIsFinished(state.isFinished());
this.atData.setHadFatalError(state.hadFatalError());
this.atData.setIsFrozen(state.isFrozen());
Long frozenBalance = state.getFrozenBalance();
this.atData.setFrozenBalance(frozenBalance != null ? BigDecimal.valueOf(frozenBalance, 8) : null);
this.repository.getATRepository().save(this.atData);
}
public void revert(int blockHeight, long blockTimestamp) throws DataException {
String atAddress = this.atData.getATAddress();
// Delete old AT state data from repository
this.repository.getATRepository().delete(atAddress, blockHeight);
if (this.atStateData.isInitial())
return;
// Load previous state data
ATStateData previousStateData = this.repository.getATRepository().getLatestATState(atAddress);
if (previousStateData == null)
throw new DataException("Can't find previous AT state data for " + atAddress);
// [Re]create AT machine state using AT state data or from scratch as applicable
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] codeBytes = this.atData.getCodeBytes();
MachineState state = MachineState.fromBytes(api, loggerFactory, previousStateData.getStateData(), codeBytes);
// Update AT info in repository
this.atData.setIsSleeping(state.isSleeping());
this.atData.setSleepUntilHeight(state.getSleepUntilHeight());
this.atData.setIsFinished(state.isFinished());
this.atData.setHadFatalError(state.hadFatalError());
this.atData.setIsFrozen(state.isFrozen());
Long frozenBalance = state.getFrozenBalance();
this.atData.setFrozenBalance(frozenBalance != null ? BigDecimal.valueOf(frozenBalance, 8) : null);
this.repository.getATRepository().save(this.atData);
}
}

View File

@ -1,12 +1,13 @@
package org.qortal.at;
import java.math.BigDecimal;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ciyam.at.API;
import org.ciyam.at.ExecutionException;
import org.ciyam.at.FunctionData;
@ -44,6 +45,7 @@ import com.google.common.primitives.Bytes;
public class QortalATAPI extends API {
private static final byte[] ADDRESS_PADDING = new byte[32 - Account.ADDRESS_LENGTH];
private static final Logger LOGGER = LogManager.getLogger(QortalATAPI.class);
// Properties
private Repository repository;
@ -128,13 +130,14 @@ public class QortalATAPI extends API {
throw new RuntimeException("AT API unable to fetch previous block hash?");
// Block's signature is 128 bytes so we need to reduce this to 4 longs (32 bytes)
// To be able to use hash to look up block, save height (8 bytes) and rehash with SHA192 (24 bytes)
// To be able to use hash to look up block, save height (8 bytes) and partial signature (24 bytes)
this.setA1(state, previousBlockHeight);
byte[] sigHash192 = sha192(blockSummaries.get(0).getSignature());
this.setA2(state, fromBytes(sigHash192, 0));
this.setA3(state, fromBytes(sigHash192, 8));
this.setA4(state, fromBytes(sigHash192, 16));
byte[] signature = blockSummaries.get(0).getSignature();
// Save some of minter's signature and transactions signature, so middle 24 bytes of the full 128 byte signature.
this.setA2(state, fromBytes(signature, 52));
this.setA3(state, fromBytes(signature, 60));
this.setA4(state, fromBytes(signature, 68));
} catch (DataException e) {
throw new RuntimeException("AT API unable to fetch previous block?", e);
}
@ -143,7 +146,7 @@ public class QortalATAPI extends API {
@Override
public void putTransactionAfterTimestampIntoA(Timestamp timestamp, MachineState state) {
// Recipient is this AT
String recipient = this.atData.getATAddress();
String atAddress = this.atData.getATAddress();
int height = timestamp.blockHeight;
int sequence = timestamp.transactionSequence + 1;
@ -151,39 +154,44 @@ public class QortalATAPI extends API {
BlockRepository blockRepository = this.getRepository().getBlockRepository();
try {
Account recipientAccount = new Account(this.getRepository(), recipient);
int currentHeight = blockRepository.getBlockchainHeight();
List<Transaction> blockTransactions = null;
while (height <= currentHeight) {
BlockData blockData = blockRepository.fromHeight(height);
if (blockTransactions == null) {
BlockData blockData = blockRepository.fromHeight(height);
if (blockData == null)
throw new DataException("Unable to fetch block " + height + " from repository?");
if (blockData == null)
throw new DataException("Unable to fetch block " + height + " from repository?");
Block block = new Block(this.getRepository(), blockData);
Block block = new Block(this.getRepository(), blockData);
List<Transaction> blockTransactions = block.getTransactions();
blockTransactions = block.getTransactions();
}
// No more transactions in this block? Try next block
if (sequence >= blockTransactions.size()) {
++height;
sequence = 0;
blockTransactions = null;
continue;
}
Transaction transaction = blockTransactions.get(sequence);
// Transaction needs to be sent to specified recipient
if (transaction.getRecipientAccounts().contains(recipientAccount)) {
List<Account> recipientAccounts = transaction.getRecipientAccounts();
List<String> recipientAddresses = recipientAccounts.stream().map(Account::getAddress).collect(Collectors.toList());
if (recipientAddresses.contains(atAddress)) {
// Found a transaction
this.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue());
// Hash transaction's signature into other three A fields for future verification that it's the same transaction
byte[] sigHash192 = sha192(transaction.getTransactionData().getSignature());
this.setA2(state, fromBytes(sigHash192, 0));
this.setA3(state, fromBytes(sigHash192, 8));
this.setA4(state, fromBytes(sigHash192, 16));
// Copy transaction's partial signature into the other three A fields for future verification that it's the same transaction
byte[] signature = transaction.getTransactionData().getSignature();
this.setA2(state, fromBytes(signature, 8));
this.setA3(state, fromBytes(signature, 16));
this.setA4(state, fromBytes(signature, 24));
return;
}
@ -245,7 +253,7 @@ public class QortalATAPI extends API {
@Override
public long getTimestampFromTransactionInA(MachineState state) {
// Transaction's "timestamp" already stored in A1
Timestamp timestamp = new Timestamp(state.getA1());
Timestamp timestamp = new Timestamp(this.getA1(state));
return timestamp.longValue();
}
@ -340,11 +348,10 @@ public class QortalATAPI extends API {
@Override
public void putCreatorAddressIntoB(MachineState state) {
// Simply use raw public key
byte[] publicKey = atData.getCreatorPublicKey();
String address = Crypto.toAddress(publicKey);
byte[] addressBytes = Bytes.ensureCapacity(address.getBytes(), 32, 0);
this.setB(state, addressBytes);
this.setB(state, publicKey);
}
@Override
@ -377,7 +384,7 @@ public class QortalATAPI extends API {
@Override
public void messageAToB(MachineState state) {
byte[] message = state.getA();
byte[] message = this.getA(state);
Account recipient = getAccountFromB(state);
long timestamp = this.getNextTransactionTimestamp();
@ -404,6 +411,9 @@ public class QortalATAPI extends API {
@Override
public void onFinished(long finalBalance, MachineState state) {
if (finalBalance <= 0)
return;
// Refund remaining balance (if any) to AT's creator
Account creator = this.getCreator();
long timestamp = this.getNextTransactionTimestamp();
@ -421,7 +431,7 @@ public class QortalATAPI extends API {
@Override
public void onFatalError(MachineState state, ExecutionException e) {
state.getLogger().error("AT " + this.atData.getATAddress() + " suffered fatal error: " + e.getMessage());
LOGGER.error("AT " + this.atData.getATAddress() + " suffered fatal error: " + e.getMessage());
}
@Override
@ -432,7 +442,7 @@ public class QortalATAPI extends API {
if (qortalFunctionCode == null)
throw new IllegalFunctionCodeException("Unknown Qortal function code 0x" + String.format("%04x", rawFunctionCode) + " encountered");
qortalFunctionCode.preExecuteCheck(2, true, state, rawFunctionCode);
qortalFunctionCode.preExecuteCheck(paramCount, returnValueExpected, rawFunctionCode);
}
@Override
@ -450,29 +460,23 @@ public class QortalATAPI extends API {
| (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56;
}
/** Returns SHA2-192 digest of input - used to verify transaction signatures */
public static byte[] sha192(byte[] input) {
try {
// SHA2-192
MessageDigest sha192 = MessageDigest.getInstance("SHA-192");
return sha192.digest(input);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-192 not available");
}
/** 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);
}
/** Verify transaction's SHA2-192 hashed signature matches A2 thru A4 */
private static void verifyTransaction(TransactionData transactionData, MachineState state) {
// Compare SHA2-192 of transaction's signature against A2 thru A4
byte[] hash = sha192(transactionData.getSignature());
/** Verify transaction's partial signature matches A2 thru A4 */
private void verifyTransaction(TransactionData transactionData, MachineState state) {
// Compare end of transaction's signature against A2 thru A4
byte[] sig = transactionData.getSignature();
if (state.getA2() != fromBytes(hash, 0) || state.getA3() != fromBytes(hash, 8) || state.getA4() != fromBytes(hash, 16))
if (this.getA2(state) != fromBytes(sig, 8) || this.getA3(state) != fromBytes(sig, 16) || this.getA4(state) != fromBytes(sig, 24))
throw new IllegalStateException("Transaction signature in A no longer matches signature from repository");
}
/** Returns transaction data from repository using block height & sequence from A1, checking the transaction signatures match too */
/* package */ TransactionData getTransactionFromA(MachineState state) {
Timestamp timestamp = new Timestamp(state.getA1());
Timestamp timestamp = new Timestamp(this.getA1(state));
try {
TransactionData transactionData = this.repository.getTransactionRepository().fromHeightAndSequence(timestamp.blockHeight,
@ -503,11 +507,11 @@ public class QortalATAPI extends API {
/** Returns the timestamp to use for next AT Transaction */
private long getNextTransactionTimestamp() {
/*
* Timestamp is block's timestamp + position in AT-Transactions list.
* Use block's timestamp.
*
* We need increasing timestamps to preserve transaction order and hence a correct signature-reference chain when the block is processed.
* This is OK because AT transactions are always generated locally and order is preserved in Transaction.getDataComparator().
*/
return this.blockTimestamp + this.transactions.size();
return this.blockTimestamp;
}
/** Returns AT account's lastReference, taking newly generated ATTransactions into account */
@ -535,7 +539,7 @@ public class QortalATAPI extends API {
* Otherwise, assume B is a public key.
*/
private Account getAccountFromB(MachineState state) {
byte[] bBytes = state.getB();
byte[] bBytes = this.getB(state);
if ((bBytes[0] == Crypto.ADDRESS_VERSION || bBytes[0] == Crypto.AT_ADDRESS_VERSION)
&& Arrays.mismatch(bBytes, Account.ADDRESS_LENGTH, 32, ADDRESS_PADDING, 0, ADDRESS_PADDING.length) == -1) {
@ -550,4 +554,14 @@ public class QortalATAPI extends API {
return new PublicKeyAccount(this.repository, bBytes);
}
/* Convenience methods to allow QortalFunctionCode package-visibility access to A/B-get/set methods. */
protected byte[] getB(MachineState state) {
return super.getB(state);
}
protected void setB(MachineState state, byte[] bBytes) {
super.setB(state, bBytes);
}
}

View File

@ -1,26 +0,0 @@
package org.qortal.at;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class QortalATLogger implements org.ciyam.at.LoggerInterface {
// NOTE: We're logging on behalf of org.qortal.at.AT, not ourselves!
private static final Logger LOGGER = LogManager.getLogger(AT.class);
@Override
public void error(String message) {
LOGGER.error(message);
}
@Override
public void debug(String message) {
LOGGER.debug(message);
}
@Override
public void echo(String message) {
LOGGER.info(message);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
package org.qortal.at;
import org.ciyam.at.AtLogger;
public class QortalAtLoggerFactory implements org.ciyam.at.AtLoggerFactory {
private static QortalAtLoggerFactory instance;
private QortalAtLoggerFactory() {
}
public static synchronized QortalAtLoggerFactory getInstance() {
if (instance == null)
instance = new QortalAtLoggerFactory();
return instance;
}
@Override
public AtLogger create(final Class<?> loggerName) {
return QortalAtLogger.create(loggerName);
}
}

View File

@ -4,6 +4,8 @@ import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ciyam.at.ExecutionException;
import org.ciyam.at.FunctionData;
import org.ciyam.at.IllegalFunctionCodeException;
@ -30,9 +32,9 @@ public enum QortalFunctionCode {
byte[] pkh = new byte[32];
// Copy PKH part of B to last 20 bytes
System.arraycopy(state.getB(), 32 - 20 - 4, pkh, 32 - 20, 20);
System.arraycopy(getB(state), 32 - 20 - 4, pkh, 32 - 20, 20);
state.getAPI().setB(state, pkh);
setB(state, pkh);
}
},
/**
@ -64,6 +66,8 @@ public enum QortalFunctionCode {
public final int paramCount;
public final boolean returnsValue;
private static final Logger LOGGER = LogManager.getLogger(QortalFunctionCode.class);
private static final Map<Short, QortalFunctionCode> map = Arrays.stream(QortalFunctionCode.values())
.collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode));
@ -77,7 +81,7 @@ public enum QortalFunctionCode {
return map.get((short) value);
}
public void preExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode) throws IllegalFunctionCodeException {
public void preExecuteCheck(int paramCount, boolean returnValueExpected, short rawFunctionCode) throws IllegalFunctionCodeException {
if (paramCount != this.paramCount)
throw new IllegalFunctionCodeException(
"Passed paramCount (" + paramCount + ") does not match function's required paramCount (" + this.paramCount + ")");
@ -100,7 +104,7 @@ public enum QortalFunctionCode {
*/
public void execute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
// Check passed functionData against requirements of this function
preExecuteCheck(functionData.paramCount, functionData.returnValueExpected, state, rawFunctionCode);
preExecuteCheck(functionData.paramCount, functionData.returnValueExpected, rawFunctionCode);
if (functionData.paramCount >= 1 && functionData.value1 == null)
throw new IllegalFunctionCodeException("Passed value1 is null but function has paramCount of (" + this.paramCount + ")");
@ -108,7 +112,7 @@ public enum QortalFunctionCode {
if (functionData.paramCount == 2 && functionData.value2 == null)
throw new IllegalFunctionCodeException("Passed value2 is null but function has paramCount of (" + this.paramCount + ")");
state.getLogger().debug("Function \"" + this.name() + "\"");
LOGGER.debug(() -> String.format("Function \"%s\"", this.name()));
postCheckExecute(functionData, state, rawFunctionCode);
}
@ -119,7 +123,7 @@ public enum QortalFunctionCode {
private static void convertAddressInB(byte addressPrefix, MachineState state) {
byte[] addressNoChecksum = new byte[1 + 20];
addressNoChecksum[0] = addressPrefix;
System.arraycopy(state.getB(), 0, addressNoChecksum, 1, 20);
System.arraycopy(getB(state), 0, addressNoChecksum, 1, 20);
byte[] checksum = Crypto.doubleDigest(addressNoChecksum);
@ -128,7 +132,17 @@ public enum QortalFunctionCode {
System.arraycopy(addressNoChecksum, 0, address, 32 - 1 - 20 - 4, addressNoChecksum.length);
System.arraycopy(checksum, 0, address, 32 - 4, 4);
state.getAPI().setB(state, address);
setB(state, address);
}
private static byte[] getB(MachineState state) {
QortalATAPI api = (QortalATAPI) state.getAPI();
return api.getB(state);
}
private static void setB(MachineState state, byte[] bBytes) {
QortalATAPI api = (QortalATAPI) state.getAPI();
api.setB(state, bBytes);
}
}

View File

@ -506,8 +506,10 @@ public class Block {
// Allocate cache for results
List<TransactionData> transactionsData = this.repository.getBlockRepository().getTransactionsFromSignature(this.blockData.getSignature());
// The number of transactions fetched from repository should correspond with Block's transactionCount
if (transactionsData.size() != this.blockData.getTransactionCount())
long nonAtTransactionCount = transactionsData.stream().filter(transactionData -> transactionData.getType() != TransactionType.AT).count();
// The number of non-AT transactions fetched from repository should correspond with Block's transactionCount
if (nonAtTransactionCount != this.blockData.getTransactionCount())
throw new IllegalStateException("Block's transactions from repository do not match block's transaction count");
this.transactions = new ArrayList<>();
@ -540,8 +542,10 @@ public class Block {
// Allocate cache for results
List<ATStateData> atStateData = this.repository.getATRepository().getBlockATStatesAtHeight(this.blockData.getHeight());
// The number of AT states fetched from repository should correspond with Block's atCount
if (atStateData.size() != this.blockData.getATCount())
// The number of non-initial AT states fetched from repository should correspond with Block's atCount.
// We exclude initial AT states created by processing DEPLOY_AT transactions as they are never serialized and so not included in block's AT count.
int nonInitialCount = (int) atStateData.stream().filter(atState -> !atState.isInitial()).count();
if (nonInitialCount != this.blockData.getATCount())
throw new IllegalStateException("Block's AT states from repository do not match block's AT count");
this.atStates = atStateData;
@ -1182,7 +1186,7 @@ public class Block {
// Run each AT, appends AT-Transactions and corresponding AT states, to our lists
for (ATData atData : executableATs) {
AT at = new AT(this.repository, atData);
List<AtTransaction> atTransactions = at.run(this.blockData.getTimestamp());
List<AtTransaction> atTransactions = at.run(this.blockData.getHeight(), this.blockData.getTimestamp());
allAtTransactions.addAll(atTransactions);
@ -1192,14 +1196,16 @@ public class Block {
this.ourAtFees = this.ourAtFees.add(atStateData.getFees());
}
// AT Transactions never need approval
allAtTransactions.forEach(transaction -> transaction.getTransactionData().setApprovalStatus(ApprovalStatus.NOT_REQUIRED));
// Prepend our entire AT-Transactions/states to block's transactions
this.transactions.addAll(0, allAtTransactions);
// Re-sort
this.transactions.sort(Transaction.getComparator());
// Update transaction count
this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1);
// AT Transactions do not affect block's transaction count
// We've added transactions, so recalculate transactions signature
calcTransactionsSignature();
@ -1408,13 +1414,17 @@ public class Block {
protected void processAtFeesAndStates() throws DataException {
ATRepository atRepository = this.repository.getATRepository();
for (ATStateData atState : this.getATStates()) {
Account atAccount = new Account(this.repository, atState.getATAddress());
for (ATStateData atStateData : this.getATStates()) {
Account atAccount = new Account(this.repository, atStateData.getATAddress());
// Subtract AT-generated fees from AT accounts
atAccount.setConfirmedBalance(Asset.QORT, atAccount.getConfirmedBalance(Asset.QORT).subtract(atState.getFees()));
atAccount.setConfirmedBalance(Asset.QORT, atAccount.getConfirmedBalance(Asset.QORT).subtract(atStateData.getFees()));
atRepository.save(atState);
// Update AT info with latest state
ATData atData = atRepository.fromATAddress(atStateData.getATAddress());
AT at = new AT(repository, atData, atStateData);
at.update(this.blockData.getHeight(), this.blockData.getTimestamp());
}
}
@ -1568,15 +1578,18 @@ public class Block {
protected void orphanAtFeesAndStates() throws DataException {
ATRepository atRepository = this.repository.getATRepository();
for (ATStateData atState : this.getATStates()) {
Account atAccount = new Account(this.repository, atState.getATAddress());
for (ATStateData atStateData : this.getATStates()) {
Account atAccount = new Account(this.repository, atStateData.getATAddress());
// Return AT-generated fees to AT accounts
atAccount.setConfirmedBalance(Asset.QORT, atAccount.getConfirmedBalance(Asset.QORT).add(atState.getFees()));
}
atAccount.setConfirmedBalance(Asset.QORT, atAccount.getConfirmedBalance(Asset.QORT).add(atStateData.getFees()));
// Delete ATStateData for this height
atRepository.deleteATStates(this.blockData.getHeight());
// Revert AT info to prior values
ATData atData = atRepository.fromATAddress(atStateData.getATAddress());
AT at = new AT(repository, atData, atStateData);
at.revert(this.blockData.getHeight(), this.blockData.getTimestamp());
}
}
protected void decreaseAccountLevels() throws DataException {

View File

@ -471,7 +471,7 @@ public class BlockChain {
this.unitFee = this.unitFee.setScale(8);
this.minFeePerByte = this.unitFee.divide(this.maxBytesPerUnitFee, MathContext.DECIMAL32);
this.ciyamAtSettings.feePerStep.setScale(8);
this.ciyamAtSettings.feePerStep = this.ciyamAtSettings.feePerStep.setScale(8);
// Pre-calculate cumulative blocks required for each level
int cumulativeBlocks = 0;

View File

@ -337,11 +337,9 @@ public class BlockMinter extends Thread {
this.interrupt();
}
public static void mintTestingBlock(Repository repository, PrivateKeyAccount... mintingAndOnlineAccounts) throws DataException {
if (!BlockChain.getInstance().isTestChain()) {
LOGGER.warn("Ignoring attempt to mint testing block for non-test chain!");
return;
}
public static Block mintTestingBlock(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
Controller.getInstance().ensureTestingAccountsOnline(mintingAndOnlineAccounts);
@ -372,6 +370,8 @@ public class BlockMinter extends Thread {
LOGGER.info(String.format("Minted new test block: %d", newBlock.getBlockData().getHeight()));
repository.saveChanges();
return newBlock;
} finally {
blockchainLock.unlock();
}

View File

@ -1,7 +1,10 @@
package org.qortal.crosschain;
import static org.ciyam.at.OpCode.calcOffset;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.function.Function;
import org.bitcoinj.core.Coin;
@ -17,9 +20,11 @@ import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptChunk;
import org.bitcoinj.script.ScriptOpCodes;
import org.ciyam.at.API;
import org.ciyam.at.CompilationException;
import org.ciyam.at.FunctionCode;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.qortal.account.Account;
import org.qortal.utils.Base58;
import org.qortal.utils.BitTwiddling;
@ -29,6 +34,23 @@ import com.google.common.primitives.Bytes;
public class BTCACCT {
public static final Coin DEFAULT_BTC_FEE = Coin.valueOf(1000L); // 0.00001000 BTC
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("750012c7ae79d85a97e64e94c467c7791dd76cf3050b864f3166635a21d767c6").asBytes(); // SHA256 of AT code bytes
public static class AtConstants {
public final byte[] secretHash;
public final BigDecimal initialPayout;
public final BigDecimal redeemPayout;
public final String recipient;
public final int refundMinutes;
public AtConstants(byte[] secretHash, BigDecimal initialPayout, BigDecimal redeemPayout, String recipient, int refundMinutes) {
this.secretHash = secretHash;
this.initialPayout = initialPayout;
this.redeemPayout = redeemPayout;
this.recipient = recipient;
this.refundMinutes = refundMinutes;
}
}
/*
* OP_TUCK (to copy public key to before signature)
@ -172,7 +194,8 @@ public class BTCACCT {
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, null, redeemSigScriptBuilder);
}
public static byte[] buildQortalAT(byte[] secretHash, String recipientQortalAddress, long refundMinutes, BigDecimal initialPayout) {
@SuppressWarnings("unused")
public static byte[] buildQortalAT(byte[] secretHash, String recipientQortalAddress, int refundMinutes, BigDecimal initialPayout, BigDecimal redeemPayout) {
// Labels for data segment addresses
int addrCounter = 0;
// Constants (with corresponding dataByteBuffer.put*() calls below)
@ -185,10 +208,15 @@ public class BTCACCT {
final int addrAddressPart3 = addrCounter++;
final int addrAddressPart4 = addrCounter++;
final int addrRefundMinutes = addrCounter++;
final int addrInitialPayoutAmount = addrCounter++;
final int addrRedeemPayoutAmount = addrCounter++;
final int addrExpectedTxType = addrCounter++;
final int addrHashIndex = addrCounter++;
final int addrAddressIndex = addrCounter++;
final int addrAddressTempIndex = addrCounter++;
final int addrHashTempIndex = addrCounter++;
final int addrHashTempLength = addrCounter++;
final int addrInitialPayoutAmount = addrCounter++;
final int addrExpectedTxType = addrCounter++;
final int addrEndOfConstants = addrCounter;
// Variables
final int addrRefundTimestamp = addrCounter++;
final int addrLastTimestamp = addrCounter++;
@ -220,136 +248,175 @@ public class BTCACCT {
assert dataByteBuffer.position() == addrRefundMinutes * MachineState.VALUE_SIZE : "addrRefundMinutes incorrect";
dataByteBuffer.putLong(refundMinutes);
// Initial payout amount
assert dataByteBuffer.position() == addrInitialPayoutAmount * MachineState.VALUE_SIZE : "addrInitialPayoutAmount incorrect";
dataByteBuffer.putLong(initialPayout.unscaledValue().longValue());
// Redeem payout amount
assert dataByteBuffer.position() == addrRedeemPayoutAmount * MachineState.VALUE_SIZE : "addrRedeemPayoutAmount incorrect";
dataByteBuffer.putLong(redeemPayout.unscaledValue().longValue());
// We're only interested in MESSAGE transactions
assert dataByteBuffer.position() == addrExpectedTxType * MachineState.VALUE_SIZE : "addrExpectedTxType incorrect";
dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
// Index into data segment of hash, used by GET_B_IND
assert dataByteBuffer.position() == addrHashIndex * MachineState.VALUE_SIZE : "addrHashIndex incorrect";
dataByteBuffer.putLong(addrHashPart1);
// Index into data segment of recipient address, used by SET_B_IND
assert dataByteBuffer.position() == addrAddressIndex * MachineState.VALUE_SIZE : "addrAddressIndex incorrect";
dataByteBuffer.putLong(addrAddressPart1);
// Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
assert dataByteBuffer.position() == addrAddressTempIndex * MachineState.VALUE_SIZE : "addrAddressTempIndex incorrect";
dataByteBuffer.putLong(addrAddressTemp1);
// Source location and length for hashing any passed secret
assert dataByteBuffer.position() == addrHashTempIndex * MachineState.VALUE_SIZE : "addrHashTempIndex incorrect";
dataByteBuffer.putLong(addrHashTemp1);
assert dataByteBuffer.position() == addrHashTempLength * MachineState.VALUE_SIZE : "addrHashTempLength incorrect";
dataByteBuffer.putLong(32L);
// Initial payout amount
assert dataByteBuffer.position() == addrInitialPayoutAmount * MachineState.VALUE_SIZE : "addrInitialPayoutAmount incorrect";
dataByteBuffer.putLong(initialPayout.unscaledValue().longValue());
// We're only interested in MESSAGE transactions
assert dataByteBuffer.position() == addrExpectedTxType * MachineState.VALUE_SIZE : "addrExpectedTxType incorrect";
dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
// Code labels
final int addrTxLoop = 0x0036;
final int addrCheckTx = 0x004b;
final int addrRefund = 0x00c6;
final int addrEndOfCode = 0x00cd;
Integer labelTxLoop = null;
Integer labelRefund = null;
Integer labelCheckTx = null;
int tempPC;
ByteBuffer codeByteBuffer = ByteBuffer.allocate(addrEndOfCode * 1);
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
/* Initialization */
// Two-pass version
for (int pass = 0; pass < 2; ++pass) {
codeByteBuffer.clear();
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_CREATION_TIMESTAMP.value).putInt(addrLastTimestamp);
// Calculate refund 'timestamp' by adding minutes to above 'timestamp', then save into addrRefundTimestamp
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(FunctionCode.ADD_MINUTES_TO_TIMESTAMP.value).putInt(addrRefundTimestamp)
.putInt(addrLastTimestamp).putInt(addrRefundMinutes);
try {
/* Initialization */
// Load recipient's address into B register
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrAddressPart1);
// Send initial payment to recipient so they have enough funds to message AT if all goes well
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PAY_TO_ADDRESS_IN_B.value).putInt(addrInitialPayoutAmount);
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTimestamp));
// Calculate refund 'timestamp' by adding minutes to above 'timestamp', then save into addrRefundTimestamp
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTimestamp, addrRefundMinutes));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.value);
// Load recipient's address into B register
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrAddressIndex));
// Send initial payment to recipient so they have enough funds to message AT if all goes well
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrInitialPayoutAmount));
/* Main loop */
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
// Fetch current block 'timestamp'
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_BLOCK_TIMESTAMP.value).putInt(addrBlockTimestamp);
// If we're past refund 'timestamp' then go refund everything back to AT creator
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BGE_DAT.value).putInt(addrBlockTimestamp).putInt(addrRefundTimestamp).put((byte) (addrRefund - tempPC));
/* Main loop */
/* Transaction processing loop */
assert codeByteBuffer.position() == addrTxLoop : "addrTxLoop incorrect";
// Fetch current block 'timestamp'
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
// If we're not past refund 'timestamp' then look for next transaction
codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelTxLoop)));
// We're past refund 'timestamp' so go refund everything back to AT creator
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
// Find next transaction to this AT since the last one (if any)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(addrLastTimestamp);
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_IS_ZERO.value).putInt(addrComparator);
// If addrComparator is zero (i.e. A is non-zero, transaction was found) then branch to addrCheckTx
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(addrComparator).put((byte) (addrCheckTx - tempPC));
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.value);
/* Transaction processing loop */
labelTxLoop = codeByteBuffer.position();
/* Check transaction */
assert codeByteBuffer.position() == addrCheckTx : "addrCheckTx incorrect";
// 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, addrLastTimestamp));
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrComparator));
// If addrComparator is zero (i.e. A is non-zero, transaction was found) then go check transaction
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrComparator, calcOffset(codeByteBuffer, labelCheckTx)));
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile());
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A.value).putInt(addrLastTimestamp);
// Extract transaction type (message/payment) from transaction and save type in addrTxType
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TYPE_FROM_TX_IN_A.value).putInt(addrTxType);
// If transaction type is not MESSAGE type then go look for another transaction
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrTxType).putInt(addrExpectedTxType).put((byte) (addrTxLoop - tempPC));
/* Check transaction */
labelCheckTx = codeByteBuffer.position();
/* Check transaction's sender */
// 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, addrLastTimestamp));
// Extract transaction type (message/payment) from transaction and save type in addrTxType
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxType));
// If transaction type is not MESSAGE type then go look for another transaction
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxType, addrExpectedTxType, calcOffset(codeByteBuffer, labelTxLoop)));
// Extract sender address from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B.value);
// Save B register into data segment starting at addrAddressTemp1
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B_IND.value).putInt(addrAddressTemp1);
// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp1).putInt(addrAddressPart1).put((byte) (addrTxLoop - tempPC));
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp2).putInt(addrAddressPart2).put((byte) (addrTxLoop - tempPC));
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp3).putInt(addrAddressPart3).put((byte) (addrTxLoop - tempPC));
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp4).putInt(addrAddressPart4).put((byte) (addrTxLoop - tempPC));
/* Check transaction's sender */
/* Check 'secret' in transaction's message */
// Extract sender address from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrAddressTemp1 (as pointed to by addrAddressTempIndex)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrAddressTempIndex));
// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp1, addrAddressPart1, calcOffset(codeByteBuffer, labelTxLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp2, addrAddressPart2, calcOffset(codeByteBuffer, labelTxLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp3, addrAddressPart3, calcOffset(codeByteBuffer, labelTxLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp4, addrAddressPart4, calcOffset(codeByteBuffer, labelTxLoop)));
// Extract message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B.value);
// Save B register into data segment starting at addrHashTemp1
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B_IND.value).putInt(addrHashTemp1);
// Load B register with expected hash result
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrHashPart1);
// Perform HASH160 using source data at addrHashTemp1 through addrHashTemp4. (Location and length specified via addrHashTempIndex and addrHashTemplength).
// Save the equality result (1 if they match, 0 otherwise) into addrComparator.
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(FunctionCode.CHECK_HASH160_WITH_B.value).putInt(addrComparator).putInt(addrHashTempIndex).putInt(addrHashTempLength);
// If hashes don't match, addrComparator will be zero so go find another transaction
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(addrComparator).put((byte) (addrTxLoop - tempPC));
/* Check 'secret' in transaction's message */
/* Success! Pay balance to intended recipient */
// Extract message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrHashTemp1 (as pointed to by addrHashTempIndex)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashTempIndex));
// Load B register with expected hash result (as pointed to by addrHashIndex)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashIndex));
// Perform HASH160 using source data at addrHashTemp1 through addrHashTemp4. (Location and length specified via addrHashTempIndex and addrHashTemplength).
// Save the equality result (1 if they match, 0 otherwise) into addrComparator.
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrComparator, addrHashTempIndex, addrHashTempLength));
// If hashes don't match, addrComparator will be zero so go find another transaction
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrComparator, calcOffset(codeByteBuffer, labelTxLoop)));
// Load B register with intended recipient address.
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrAddressPart1);
// Pay AT's balance to recipient
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B.value);
// We're finished forever
codeByteBuffer.put(OpCode.FIN_IMD.value);
/* Success! Pay arranged amount to intended recipient */
/* Refund balance back to AT creator */
assert codeByteBuffer.position() == addrRefund : "addrRefund incorrect";
// Load B register with intended recipient address (as pointed to by addrAddressIndex)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrAddressIndex));
// Pay AT's balance to recipient
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrRedeemPayoutAmount));
// We're finished forever
codeByteBuffer.put(OpCode.FIN_IMD.compile());
// Load B register with AT creator's address.
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_CREATOR_INTO_B.value);
// Pay AT's balance back to AT's creator.
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B.value);
// We're finished forever
codeByteBuffer.put(OpCode.FIN_IMD.value);
/* Refund balance back to AT creator */
labelRefund = codeByteBuffer.position();
// end-of-code
assert codeByteBuffer.position() == addrEndOfCode : "addrEndOfCode incorrect";
// Load B register with AT creator's address.
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B));
// Pay AT's balance back to AT's creator.
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B));
// We're finished forever
codeByteBuffer.put(OpCode.FIN_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile BTC-QORT ACCT?", 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, codeByteBuffer.array(), dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
public static AtConstants extractAtConstants(byte[] dataBytes) {
ByteBuffer dataByteBuffer = ByteBuffer.wrap(dataBytes);
byte[] secretHash = new byte[32];
dataByteBuffer.get(secretHash);
byte[] addressBytes = new byte[32];
dataByteBuffer.get(addressBytes);
String recipient = Base58.encode(Arrays.copyOf(addressBytes, Account.ADDRESS_LENGTH));
int refundMinutes = (int) dataByteBuffer.getLong();
BigDecimal initialPayout = BigDecimal.valueOf(dataByteBuffer.getLong(), 8);
BigDecimal redeemPayout = BigDecimal.valueOf(dataByteBuffer.getLong(), 8);
return new AtConstants(secretHash, initialPayout, redeemPayout, recipient, refundMinutes);
}
}

View File

@ -59,6 +59,18 @@ public class Crypto {
return Bytes.concat(digest, digest);
}
/** Returns RMD160(SHA256(data)) */
public static byte[] hash160(byte[] data) {
byte[] interim = digest(data);
try {
MessageDigest md160 = MessageDigest.getInstance("RIPEMD160");
return md160.digest(interim);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("RIPEMD160 message digest not available");
}
}
@SuppressWarnings("deprecation")
private static String toAddress(byte addressVersion, byte[] input) {
// SHA2-256 input to create new data and of known size

View File

@ -11,32 +11,38 @@ public class ATStateData {
private byte[] stateData;
private byte[] stateHash;
private BigDecimal fees;
private boolean isInitial;
// Constructors
/** Create new ATStateData */
public ATStateData(String ATAddress, Integer height, Long creation, byte[] stateData, byte[] stateHash, BigDecimal fees) {
public ATStateData(String ATAddress, Integer height, Long creation, byte[] stateData, byte[] stateHash, BigDecimal fees, boolean isInitial) {
this.ATAddress = ATAddress;
this.height = height;
this.creation = creation;
this.stateData = stateData;
this.stateHash = stateHash;
this.fees = fees;
this.isInitial = isInitial;
}
/** For recreating per-block ATStateData from repository where not all info is needed */
public ATStateData(String ATAddress, int height, byte[] stateHash, BigDecimal fees) {
this(ATAddress, height, null, null, stateHash, fees);
public ATStateData(String ATAddress, int height, byte[] stateHash, BigDecimal fees, boolean isInitial) {
this(ATAddress, height, null, 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(ATAddress, null, null, null, stateHash, null);
// 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, null, stateHash, null, false);
}
/** For creating ATStateData from serialized bytes when we don't have all the info */
public ATStateData(String ATAddress, byte[] stateHash, BigDecimal fees) {
this(ATAddress, null, null, null, stateHash, 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, null, stateHash, fees, false);
}
// Getters / setters
@ -70,4 +76,8 @@ public class ATStateData {
return this.fees;
}
public boolean isInitial() {
return this.isInitial;
}
}

View File

@ -1,11 +1,12 @@
package org.qortal.repository.hsqldb;
import static org.qortal.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli;
import static org.qortal.repository.hsqldb.HSQLDBRepository.toOffsetDateTime;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import org.qortal.data.at.ATData;
@ -25,14 +26,18 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATData fromATAddress(String atAddress) throws DataException {
final String sql = "SELECT creator, creation, version, asset_id, code_bytes, is_sleeping, sleep_until_height, is_finished, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE AT_address = ?";
String sql = "SELECT creator, creation, version, asset_id, code_bytes, "
+ "is_sleeping, sleep_until_height, is_finished, had_fatal_error, "
+ "is_frozen, frozen_balance "
+ "FROM ATs "
+ "WHERE AT_address = ? LIMIT 1";
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
if (resultSet == null)
return null;
byte[] creatorPublicKey = resultSet.getBytes(1);
long creation = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
long creation = getZonedTimestampMilli(resultSet, 2);
int version = resultSet.getInt(3);
long assetId = resultSet.getLong(4);
byte[] codeBytes = resultSet.getBytes(5); // Actually BLOB
@ -66,9 +71,14 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public List<ATData> getAllExecutableATs() throws DataException {
final String sql = "SELECT AT_address, creator, creation, version, asset_id, code_bytes, is_sleeping, sleep_until_height, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE is_finished = false ORDER BY creation ASC";
String sql = "SELECT AT_address, creator, creation, version, asset_id, code_bytes, "
+ "is_sleeping, sleep_until_height, had_fatal_error, "
+ "is_frozen, frozen_balance "
+ "FROM ATs "
+ "WHERE is_finished = false "
+ "ORDER BY creation ASC";
List<ATData> executableATs = new ArrayList<ATData>();
List<ATData> executableATs = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql)) {
if (resultSet == null)
@ -79,7 +89,7 @@ public class HSQLDBATRepository implements ATRepository {
do {
String atAddress = resultSet.getString(1);
byte[] creatorPublicKey = resultSet.getBytes(2);
long creation = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
long creation = getZonedTimestampMilli(resultSet, 3);
int version = resultSet.getInt(4);
long assetId = resultSet.getLong(5);
byte[] codeBytes = resultSet.getBytes(6); // Actually BLOB
@ -108,7 +118,12 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public Integer getATCreationBlockHeight(String atAddress) throws DataException {
final String sql = "SELECT height from DeployATTransactions JOIN BlockTransactions ON transaction_signature = signature JOIN Blocks ON Blocks.signature = block_signature WHERE AT_address = ?";
String sql = "SELECT height "
+ "FROM DeployATTransactions "
+ "JOIN BlockTransactions ON transaction_signature = signature "
+ "JOIN Blocks ON Blocks.signature = block_signature "
+ "WHERE AT_address = ? "
+ "LIMIT 1";
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
if (resultSet == null)
@ -124,7 +139,7 @@ public class HSQLDBATRepository implements ATRepository {
public void save(ATData atData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("ATs");
saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).bind("creation", new Timestamp(atData.getCreation()))
saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).bind("creation", toOffsetDateTime(atData.getCreation()))
.bind("version", atData.getVersion()).bind("asset_id", atData.getAssetId()).bind("code_bytes", atData.getCodeBytes())
.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())
@ -151,17 +166,22 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT creation, state_data, state_hash, fees FROM ATStates WHERE AT_address = ? AND height = ?", atAddress, height)) {
String sql = "SELECT creation, state_data, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE AT_address = ? AND height = ? "
+ "LIMIT 1";
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress, height)) {
if (resultSet == null)
return null;
long creation = resultSet.getTimestamp(1, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
long creation = getZonedTimestampMilli(resultSet, 1);
byte[] stateData = resultSet.getBytes(2); // Actually BLOB
byte[] stateHash = resultSet.getBytes(3);
BigDecimal fees = resultSet.getBigDecimal(4);
boolean isInitial = resultSet.getBoolean(5);
return new ATStateData(atAddress, height, creation, stateData, stateHash, fees);
return new ATStateData(atAddress, height, creation, stateData, stateHash, fees, isInitial);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT state from repository", e);
}
@ -169,18 +189,24 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATStateData getLatestATState(String atAddress) throws DataException {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT height, creation, state_data, state_hash, fees FROM ATStates WHERE AT_address = ? ORDER BY height DESC", atAddress)) {
String sql = "SELECT height, creation, state_data, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE AT_address = ? "
+ "ORDER BY height DESC "
+ "LIMIT 1";
try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) {
if (resultSet == null)
return null;
int height = resultSet.getInt(1);
long creation = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
long creation = getZonedTimestampMilli(resultSet, 2);
byte[] stateData = resultSet.getBytes(3); // Actually BLOB
byte[] stateHash = resultSet.getBytes(4);
BigDecimal fees = resultSet.getBigDecimal(5);
boolean isInitial = resultSet.getBoolean(6);
return new ATStateData(atAddress, height, creation, stateData, stateHash, fees);
return new ATStateData(atAddress, height, creation, stateData, stateHash, fees, isInitial);
} catch (SQLException e) {
throw new DataException("Unable to fetch latest AT state from repository", e);
}
@ -188,10 +214,14 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
String sql = "SELECT AT_address, state_hash, fees, is_initial "
+ "FROM ATStates "
+ "WHERE height = ? "
+ "ORDER BY creation ASC";
List<ATStateData> atStates = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT AT_address, state_hash, fees FROM ATStates WHERE height = ? ORDER BY creation ASC",
height)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql, height)) {
if (resultSet == null)
return atStates; // No atStates in this block
@ -200,8 +230,9 @@ public class HSQLDBATRepository implements ATRepository {
String atAddress = resultSet.getString(1);
byte[] stateHash = resultSet.getBytes(2);
BigDecimal fees = resultSet.getBigDecimal(3);
boolean isInitial = resultSet.getBoolean(4);
ATStateData atStateData = new ATStateData(atAddress, height, stateHash, fees);
ATStateData atStateData = new ATStateData(atAddress, height, stateHash, fees, isInitial);
atStates.add(atStateData);
} while (resultSet.next());
} catch (SQLException e) {
@ -220,8 +251,9 @@ public class HSQLDBATRepository implements ATRepository {
HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates");
saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
.bind("creation", new Timestamp(atStateData.getCreation())).bind("state_data", atStateData.getStateData())
.bind("state_hash", atStateData.getStateHash()).bind("fees", atStateData.getFees());
.bind("creation", toOffsetDateTime(atStateData.getCreation())).bind("state_data", atStateData.getStateData())
.bind("state_hash", atStateData.getStateHash()).bind("fees", atStateData.getFees())
.bind("is_initial", atStateData.isInitial());
try {
saveHelper.execute(this.repository);

View File

@ -975,6 +975,11 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CHECKPOINT DEFRAG");
break;
case 71:
// Add flag to AT state data to indicate 'initial deployment state'
stmt.execute("ALTER TABLE ATStates ADD COLUMN is_initial BOOLEAN NOT NULL DEFAULT TRUE");
break;
default:
// nothing to do
return false;

View File

@ -178,6 +178,8 @@ public class AtTransaction extends Transaction {
@Override
public void processReferencesAndFees() throws DataException {
getATAccount().setLastReference(this.atTransactionData.getSignature());
if (this.atTransactionData.getAmount() != null) {
Account recipient = getRecipient();
long assetId = this.atTransactionData.getAssetId();
@ -211,6 +213,8 @@ public class AtTransaction extends Transaction {
@Override
public void orphanReferencesAndFees() throws DataException {
getATAccount().setLastReference(this.atTransactionData.getReference());
if (this.atTransactionData.getAmount() != null) {
Account recipient = getRecipient();

View File

@ -13,6 +13,7 @@ import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.at.AT;
import org.qortal.at.QortalATAPI;
import org.qortal.at.QortalAtLoggerFactory;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.data.asset.AssetData;
@ -212,9 +213,10 @@ public class DeployAtTransaction extends Transaction {
long blockTimestamp = Timestamp.toLong(height, 0);
QortalATAPI api = new QortalATAPI(repository, skeletonAtData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
try {
new MachineState(api, deployATTransactionData.getCreationBytes());
new MachineState(api, loggerFactory, deployATTransactionData.getCreationBytes());
} catch (IllegalArgumentException e) {
// Not valid
return ValidationResult.INVALID_CREATION_BYTES;

View File

@ -1020,11 +1020,16 @@ public abstract class Transaction {
// AT transactions come before non-AT transactions
if (td1.getType() == TransactionType.AT && td2.getType() != TransactionType.AT)
return -1;
// Non-AT transactions come after AT transactions
if (td1.getType() != TransactionType.AT && td2.getType() == TransactionType.AT)
return 1;
// Both transactions are either AT or non-AT so compare timestamps
// If both transactions are AT type, then preserve existing ordering.
if (td1.getType() == TransactionType.AT)
return 0;
// Both transactions are non-AT so compare timestamps
int result = Long.compare(td1.getTimestamp(), td2.getTimestamp());
if (result == 0)

View File

@ -13,17 +13,23 @@ import org.qortal.settings.Settings;
public class orphan {
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("usage: orphan <new-blockchain-tip-height>");
if (args.length < 1 || args.length > 2) {
System.err.println("usage: orphan [<settings-file>] <new-blockchain-tip-height>");
System.exit(1);
}
int targetHeight = Integer.parseInt(args[0]);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
// Load/check settings, which potentially sets up blockchain config, etc.
Settings.getInstance();
int argIndex = 0;
if (args.length > 1) {
Settings.fileInstance(args[argIndex++]);
} else {
// Load/check settings, which potentially sets up blockchain config, etc.
Settings.getInstance();
}
int targetHeight = Integer.parseInt(args[argIndex]);
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());

View File

@ -0,0 +1,403 @@
package org.qortal.test.btcacct;
import static org.junit.Assert.assertTrue;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import org.ciyam.at.MachineState;
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.QortalAtLoggerFactory;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
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 com.google.common.hash.HashCode;
public class AtTests extends Common {
public static final byte[] secret = "This string is exactly 32 bytes!".getBytes();
public static final byte[] secretHash = Crypto.hash160(secret); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final int refundTimeout = 10; // blocks
public static final BigDecimal initialPayout = new BigDecimal("0.001").setScale(8);
public static final BigDecimal redeemAmount = new BigDecimal("80.4020").setScale(8);
public static final BigDecimal fundingAmount = new BigDecimal("123.456").setScale(8);
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testCompile() {
String redeemAddress = Common.getTestAccount(null, "chloe").getAddress();
byte[] creationBytes = BTCACCT.buildQortalAT(secretHash, redeemAddress, refundTimeout, initialPayout, redeemAmount);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
}
@Test
public void testDeploy() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient);
BigDecimal expectedBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtTransaction.getTransactionData().getFee());
BigDecimal actualBalance = deployer.getBalance(Asset.QORT);
Common.assertEqualBigDecimals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = fundingAmount;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
Common.assertEqualBigDecimals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = recipientsInitialBalance;
actualBalance = recipient.getBalance(Asset.QORT);
Common.assertEqualBigDecimals("Recipient's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = deployersInitialBalance;
actualBalance = deployer.getBalance(Asset.QORT);
Common.assertEqualBigDecimals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = BigDecimal.ZERO;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
Common.assertEqualBigDecimals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = recipientsInitialBalance;
actualBalance = recipient.getBalance(Asset.QORT);
Common.assertEqualBigDecimals("Recipient's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testInitialPayment() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient);
// Initial payment should happen 1st block after deployment
BlockUtils.mintBlock(repository);
BigDecimal expectedBalance = recipientsInitialBalance.add(initialPayout);
BigDecimal actualBalance = recipient.getConfirmedBalance(Asset.QORT);
Common.assertEqualBigDecimals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = recipientsInitialBalance;
actualBalance = recipient.getBalance(Asset.QORT);
Common.assertEqualBigDecimals("Recipient's pre-initial-payout balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testAutomaticRefund() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient);
BigDecimal deployAtFee = deployAtTransaction.getTransactionData().getFee();
BigDecimal deployersPostDeploymentBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtFee);
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
BigDecimal expectedBalance = deployersPostDeploymentBalance;
BigDecimal actualBalance = deployer.getBalance(Asset.QORT);
Common.assertEqualBigDecimals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient);
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
// Send correct secret to AT
MessageTransaction messageTransaction = sendSecret(repository, recipient, secret, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
BigDecimal expectedBalance = recipientsInitialBalance.add(initialPayout).subtract(messageTransaction.getTransactionData().getFee()).add(redeemAmount);
BigDecimal actualBalance = recipient.getConfirmedBalance(Asset.QORT);
Common.assertEqualBigDecimals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
expectedBalance = recipientsInitialBalance.add(initialPayout).subtract(messageTransaction.getTransactionData().getFee());
actualBalance = recipient.getBalance(Asset.QORT);
Common.assertEqualBigDecimals("Recipent's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient);
BigDecimal deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
// Send correct secret to AT, but from wrong account
MessageTransaction messageTransaction = sendSecret(repository, bystander, secret, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
BigDecimal expectedBalance = recipientsInitialBalance.add(initialPayout);
BigDecimal actualBalance = recipient.getConfirmedBalance(Asset.QORT);
Common.assertEqualBigDecimals("Recipent's balance incorrect", expectedBalance, actualBalance);
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient);
BigDecimal deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
// Send correct secret to AT, but from wrong account
byte[] wrongSecret = Crypto.digest(secret);
MessageTransaction messageTransaction = sendSecret(repository, recipient, wrongSecret, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
BigDecimal expectedBalance = recipientsInitialBalance.add(initialPayout).subtract(messageTransaction.getTransactionData().getFee());
BigDecimal actualBalance = recipient.getConfirmedBalance(Asset.QORT);
Common.assertEqualBigDecimals("Recipent's balance incorrect", expectedBalance, actualBalance);
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
BigDecimal deployersInitialBalance = deployer.getBalance(Asset.QORT);
BigDecimal recipientsInitialBalance = recipient.getBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, recipient);
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
for (ATData atData : executableAts) {
String atAddress = atData.getATAddress();
byte[] codeBytes = atData.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
atAddress,
codeBytes.length,
(codeBytes.length != 1 ? "s": ""),
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, BTCACCT.CODE_BYTES_HASH))
continue;
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData);
BTCACCT.AtConstants atConstants = BTCACCT.extractAtConstants(dataBytes);
long autoRefundTimestamp = atData.getCreation() + atConstants.refundMinutes * 60 * 1000L;
String autoRefundString = LocalDateTime.ofInstant(Instant.ofEpochMilli(autoRefundTimestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
System.out.println(String.format("%s:\n"
+ "\tcreator: %s,\n"
+ "\tHASH160 of secret: %s,\n"
+ "\trecipient: %s,\n"
+ "\tinitial payout: %s QORT,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\tauto-refund at: %s (local time)",
atAddress,
Crypto.toAddress(atData.getCreatorPublicKey()),
HashCode.fromBytes(atConstants.secretHash).toString().substring(0, 40),
atConstants.recipient,
atConstants.initialPayout.toPlainString(),
atConstants.redeemPayout.toPlainString(),
autoRefundString));
}
}
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, Account recipient) throws DataException {
byte[] creationBytes = BTCACCT.buildQortalAT(secretHash, recipient.getAddress(), refundTimeout, initialPayout, redeemAmount);
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);
}
BigDecimal fee = BigDecimal.ZERO;
String name = "QORT-BTC cross-chain trade";
String description = String.format("Qortal-Bitcoin cross-chain trade between %s and %s", deployer.getAddress(), recipient.getAddress());
String atType = "ACCT";
String tags = "QORT-BTC ACCT";
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 MessageTransaction sendSecret(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);
}
BigDecimal fee = BigDecimal.ZERO;
BigDecimal amount = BigDecimal.ZERO;
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, 4, recipient, Asset.QORT, amount, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
private void checkAtRefund(Repository repository, Account deployer, BigDecimal deployersInitialBalance, BigDecimal deployAtFee) throws DataException {
BigDecimal deployersPostDeploymentBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtFee);
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
BlockUtils.mintBlock(repository);
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
BigDecimal expectedMinimumBalance = deployersPostDeploymentBalance;
BigDecimal expectedMaximumBalance = deployersInitialBalance.subtract(deployAtFee).subtract(initialPayout);
BigDecimal actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance.toPlainString(), expectedMinimumBalance.toPlainString()), actualBalance.compareTo(expectedMinimumBalance) > 0);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance.toPlainString(), expectedMaximumBalance.toPlainString()), actualBalance.compareTo(expectedMaximumBalance) < 0);
}
}

View File

@ -34,7 +34,7 @@ public class BuildP2SH {
+ "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
+ "\t0.00008642 \\\n"
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
+ "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t1585920000"));
System.exit(1);
}

View File

@ -37,7 +37,7 @@ public class CheckP2SH {
+ "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
+ "\t0.00008642 \\\n"
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
+ "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t1585920000"));
System.exit(1);
}

View File

@ -32,34 +32,37 @@ import com.google.common.hash.HashCode;
public class DeployAT {
public static final BigDecimal atFundingExtra = new BigDecimal("0.2").setScale(8);
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <redeem Qortal address> <HASH160-of-secret> <locktime> (<initial QORT payout>)"));
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <redeem Qortal address> <HASH160-of-secret> <locktime> [<initial QORT payout> [<AT funding amount>]]"));
System.err.println(String.format("example: DeployAT "
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
+ "\t3.1415 \\\n"
+ "\tQgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v \\\n"
+ "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t1585920000 \\\n"
+ "\t0.0001"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 5 || args.length > 6)
if (args.length < 5 || args.length > 7)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Settings.fileInstance("settings-test.json");
byte[] refundPrivateKey = null;
BigDecimal qortAmount = null;
BigDecimal redeemAmount = null;
String redeemAddress = null;
byte[] secretHash = null;
int lockTime = 0;
BigDecimal initialPayout = BigDecimal.ZERO.setScale(8);
BigDecimal fundingAmount = null;
int argIndex = 0;
try {
@ -67,8 +70,8 @@ public class DeployAT {
if (refundPrivateKey.length != 32)
usage("Refund private key must be 32 bytes");
qortAmount = new BigDecimal(args[argIndex++]);
if (qortAmount.signum() <= 0)
redeemAmount = new BigDecimal(args[argIndex++]).setScale(8);
if (redeemAmount.signum() <= 0)
usage("QORT amount must be positive");
redeemAddress = args[argIndex++];
@ -83,6 +86,13 @@ public class DeployAT {
if (args.length > argIndex)
initialPayout = new BigDecimal(args[argIndex++]).setScale(8);
if (args.length > argIndex) {
fundingAmount = new BigDecimal(args[argIndex++]).setScale(8);
if (fundingAmount.compareTo(redeemAmount) <= 0)
usage("AT funding amount must be greater than QORT redeem amount");
}
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
@ -100,7 +110,11 @@ public class DeployAT {
PrivateKeyAccount refundAccount = new PrivateKeyAccount(repository, refundPrivateKey);
System.out.println(String.format("Refund Qortal address: %s", refundAccount.getAddress()));
System.out.println(String.format("QORT amount (INCLUDING FEES): %s", qortAmount.toPlainString()));
System.out.println(String.format("QORT redeem amount: %s", redeemAmount.toPlainString()));
if (fundingAmount == null)
fundingAmount = redeemAmount.add(atFundingExtra);
System.out.println(String.format("AT funding amount: %s", fundingAmount.toPlainString()));
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
@ -117,7 +131,7 @@ public class DeployAT {
final int BLOCK_TIME = 60; // seconds
final int refundTimeout = (lockTime - (int) (System.currentTimeMillis() / 1000L)) / BLOCK_TIME;
byte[] creationBytes = BTCACCT.buildQortalAT(secretHash, redeemAddress, refundTimeout, initialPayout);
byte[] creationBytes = BTCACCT.buildQortalAT(secretHash, redeemAddress, refundTimeout, initialPayout, fundingAmount);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
long txTimestamp = System.currentTimeMillis();
@ -135,7 +149,7 @@ public class DeployAT {
String tags = "QORT-BTC ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, refundAccount.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, qortAmount, Asset.QORT);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, redeemAmount, Asset.QORT);
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);

View File

@ -44,7 +44,7 @@ public class Redeem {
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
+ "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
+ "\tec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03 \\\n"
+ "\t736563726574 \\\n"
+ "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n"
+ "\t1585920000"));
System.exit(1);
}

View File

@ -44,7 +44,7 @@ public class Refund {
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
+ "\tef027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c01b576fb7e \\\n"
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
+ "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t1585920000"));
System.exit(1);
}

View File

@ -17,9 +17,9 @@ public class BlockUtils {
private static final Logger LOGGER = LogManager.getLogger(BlockUtils.class);
/** Mints a new block using "alice-reward-share" test account. */
public static void mintBlock(Repository repository) throws DataException {
public static Block mintBlock(Repository repository) throws DataException {
PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, "alice-reward-share");
BlockMinter.mintTestingBlock(repository, mintingAccount);
return BlockMinter.mintTestingBlock(repository, mintingAccount);
}
public static BigDecimal getNextBlockReward(Repository repository) throws DataException {

View File

@ -29,6 +29,12 @@
"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,

View File

@ -29,6 +29,12 @@
"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,

View File

@ -29,6 +29,12 @@
"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,

View File

@ -29,6 +29,12 @@
"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,

View File

@ -29,6 +29,12 @@
"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,

View File

@ -29,6 +29,12 @@
"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,

View File

@ -2,5 +2,6 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-old-asset.json",
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
}

View File

@ -2,5 +2,6 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v1.json",
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
}

View File

@ -2,5 +2,6 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-founder-rewards.json",
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
}

View File

@ -2,5 +2,6 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-minting.json",
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
}

View File

@ -2,5 +2,6 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-qora-holder.json",
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
}

View File

@ -2,5 +2,6 @@
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2.json",
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
}