mirror of
https://github.com/Qortal/qortal.git
synced 2025-03-16 20:22:32 +00:00
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:
parent
3eaeb927ec
commit
98506a038b
5
pom.xml
5
pom.xml
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
2182
src/main/java/org/qortal/at/QortalAtLogger.java
Normal file
2182
src/main/java/org/qortal/at/QortalAtLogger.java
Normal file
File diff suppressed because it is too large
Load Diff
24
src/main/java/org/qortal/at/QortalAtLoggerFactory.java
Normal file
24
src/main/java/org/qortal/at/QortalAtLoggerFactory.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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());
|
||||
|
403
src/test/java/org/qortal/test/btcacct/AtTests.java
Normal file
403
src/test/java/org/qortal/test/btcacct/AtTests.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -34,7 +34,7 @@ public class BuildP2SH {
|
||||
+ "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
|
||||
+ "\t0.00008642 \\\n"
|
||||
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
|
||||
+ "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ public class CheckP2SH {
|
||||
+ "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
|
||||
+ "\t0.00008642 \\\n"
|
||||
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
|
||||
+ "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -44,7 +44,7 @@ public class Redeem {
|
||||
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
|
||||
+ "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
|
||||
+ "\tec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03 \\\n"
|
||||
+ "\t736563726574 \\\n"
|
||||
+ "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ public class Refund {
|
||||
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
|
||||
+ "\tef027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c01b576fb7e \\\n"
|
||||
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
|
||||
+ "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n"
|
||||
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -2,5 +2,6 @@
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-old-asset.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"testNtpOffset": 0,
|
||||
"minPeers": 0
|
||||
}
|
||||
|
@ -2,5 +2,6 @@
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v1.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"testNtpOffset": 0,
|
||||
"minPeers": 0
|
||||
}
|
||||
|
@ -2,5 +2,6 @@
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2-founder-rewards.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"testNtpOffset": 0,
|
||||
"minPeers": 0
|
||||
}
|
||||
|
@ -2,5 +2,6 @@
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2-minting.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"testNtpOffset": 0,
|
||||
"minPeers": 0
|
||||
}
|
||||
|
@ -2,5 +2,6 @@
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2-qora-holder.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"testNtpOffset": 0,
|
||||
"minPeers": 0
|
||||
}
|
||||
|
@ -2,5 +2,6 @@
|
||||
"restrictedApi": false,
|
||||
"blockchainConfig": "src/test/resources/test-chain-v2.json",
|
||||
"wipeUnconfirmedOnStart": false,
|
||||
"testNtpOffset": 0,
|
||||
"minPeers": 0
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user