More work on QORT-BTC ACCT

Requires fix in CIYAM AT v1.3.2

New version of Qortal cross-trade AT code.

Change how Qortal addresses are managed in QortalATAPI from using
base58 strings (that are too long) to using hex form (25 bytes)
as they need to fix into 32 byte A/B register.

Generate AT addresses using DeployAtTransaction's signature instead
of convoluted hash of AT data like name, description, etc.

Add startTime as arg to GetTransaction test app.

Add missing fields (name, description, ATType, tags) to DeployAT test app.
This commit is contained in:
catbref 2020-04-08 18:10:24 +01:00
parent 7ded8954c6
commit 3eaeb927ec
10 changed files with 192 additions and 92 deletions

View File

@ -406,7 +406,7 @@
<dependency> <dependency>
<groupId>org.ciyam</groupId> <groupId>org.ciyam</groupId>
<artifactId>at</artifactId> <artifactId>at</artifactId>
<version>1.3</version> <version>1.3.2</version>
</dependency> </dependency>
<!-- Bitcoin support --> <!-- Bitcoin support -->
<dependency> <dependency>

View File

@ -5,6 +5,7 @@ import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
import org.ciyam.at.MachineState; import org.ciyam.at.MachineState;
import org.ciyam.at.Timestamp;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData; import org.qortal.data.at.ATData;
@ -48,7 +49,13 @@ public class AT {
short version = (short) ((creationBytes[0] & 0xff) | (creationBytes[1] << 8)); // Little-endian short version = (short) ((creationBytes[0] & 0xff) | (creationBytes[1] << 8)); // Little-endian
if (version >= 2) { if (version >= 2) {
MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes()); // Just enough AT data to allow API to query initial balances, etc.
ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId);
long blockTimestamp = Timestamp.toLong(height, 0);
QortalATAPI api = new QortalATAPI(repository, skeletonAtData, blockTimestamp);
MachineState machineState = new MachineState(api, deployATTransactionData.getCreationBytes());
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(), this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(),
machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(), machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(),

View File

@ -4,6 +4,7 @@ import java.math.BigDecimal;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import org.ciyam.at.API; import org.ciyam.at.API;
@ -35,11 +36,15 @@ import org.qortal.repository.DataException;
import org.qortal.repository.Repository; import org.qortal.repository.Repository;
import org.qortal.transaction.AtTransaction; import org.qortal.transaction.AtTransaction;
import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.utils.Base58;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
public class QortalATAPI extends API { public class QortalATAPI extends API {
private static final byte[] ADDRESS_PADDING = new byte[32 - Account.ADDRESS_LENGTH];
// Properties // Properties
private Repository repository; private Repository repository;
private ATData atData; private ATData atData;
@ -316,18 +321,30 @@ public class QortalATAPI extends API {
public void putAddressFromTransactionInAIntoB(MachineState state) { public void putAddressFromTransactionInAIntoB(MachineState state) {
TransactionData transactionData = this.getTransactionFromA(state); TransactionData transactionData = this.getTransactionFromA(state);
// We actually use public key as it has more potential utility (e.g. message verification) than an address String address;
byte[] bytes = transactionData.getCreatorPublicKey(); if (transactionData.getType() == TransactionType.AT) {
// Use AT address from transaction data, as transaction's public key will always be fake
address = ((ATTransactionData) transactionData).getATAddress();
} else {
byte[] publicKey = transactionData.getCreatorPublicKey();
address = Crypto.toAddress(publicKey);
}
this.setB(state, bytes); // Convert to byte form as this only takes 25 bytes,
// compared to string-form's 34 bytes,
// and we only have 32 bytes available.
byte[] addressBytes = Bytes.ensureCapacity(Base58.decode(address), 32, 0); // pad to 32 bytes
this.setB(state, addressBytes);
} }
@Override @Override
public void putCreatorAddressIntoB(MachineState state) { public void putCreatorAddressIntoB(MachineState state) {
// We actually use public key as it has more potential utility (e.g. message verification) than an address byte[] publicKey = atData.getCreatorPublicKey();
byte[] bytes = atData.getCreatorPublicKey(); String address = Crypto.toAddress(publicKey);
byte[] addressBytes = Bytes.ensureCapacity(address.getBytes(), 32, 0);
this.setB(state, bytes); this.setB(state, addressBytes);
} }
@Override @Override
@ -490,10 +507,7 @@ public class QortalATAPI extends API {
* *
* We need increasing timestamps to preserve transaction order and hence a correct signature-reference chain when the block is processed. * We need increasing timestamps to preserve transaction order and hence a correct signature-reference chain when the block is processed.
*/ */
return this.blockTimestamp + this.transactions.size();
// XXX THE ABOVE IS NO LONGER TRUE IN QORTAL!
// return this.blockTimestamp + this.transactions.size();
throw new RuntimeException("AT timestamp code not fixed!");
} }
/** Returns AT account's lastReference, taking newly generated ATTransactions into account */ /** Returns AT account's lastReference, taking newly generated ATTransactions into account */
@ -515,21 +529,22 @@ public class QortalATAPI extends API {
/** /**
* Returns Account (possibly PublicKeyAccount) based on value in B. * Returns Account (possibly PublicKeyAccount) based on value in B.
* <p> * <p>
* If bytes in B start with 'Q' then use B as an address, but only if valid. * If first byte in B starts with either address version bytes,<br>
* and bytes 26 to 32 are zero, then use as an address, but only if valid.
* <p> * <p>
* Otherwise, assume B is a public key. * Otherwise, assume B is a public key.
* @return
*/ */
private Account getAccountFromB(MachineState state) { private Account getAccountFromB(MachineState state) {
byte[] bBytes = state.getB(); byte[] bBytes = state.getB();
if (bBytes[0] == 'Q') { if ((bBytes[0] == Crypto.ADDRESS_VERSION || bBytes[0] == Crypto.AT_ADDRESS_VERSION)
int zeroIndex = Bytes.indexOf(bBytes, (byte) 0); && Arrays.mismatch(bBytes, Account.ADDRESS_LENGTH, 32, ADDRESS_PADDING, 0, ADDRESS_PADDING.length) == -1) {
if (zeroIndex > 0) { // Extract only the bytes containing address
String address = new String(bBytes, 0, zeroIndex); byte[] addressBytes = Arrays.copyOf(bBytes, Account.ADDRESS_LENGTH);
if (Crypto.isValidAddress(address)) // If address (in byte form) is valid...
return new Account(this.repository, address); if (Crypto.isValidAddress(addressBytes))
} // ...then return an Account using address (converted to Base58
return new Account(this.repository, Base58.encode(addressBytes));
} }
return new PublicKeyAccount(this.repository, bBytes); return new PublicKeyAccount(this.repository, bBytes);

View File

@ -25,6 +25,7 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@ -466,18 +467,6 @@ public class BTC {
} }
} }
private static class TransactionStorage {
private Transaction transaction;
public void store(Transaction transaction) {
this.transaction = transaction;
}
public Transaction getTransaction() {
return this.transaction;
}
}
public List<TransactionOutput> getOutputs(byte[] txId, long startTime) { public List<TransactionOutput> getOutputs(byte[] txId, long startTime) {
Wallet wallet = createEmptyWallet(); Wallet wallet = createEmptyWallet();
@ -487,7 +476,7 @@ public class BTC {
final Sha256Hash txHash = Sha256Hash.wrap(txId); final Sha256Hash txHash = Sha256Hash.wrap(txId);
final TransactionStorage transactionStorage = new TransactionStorage(); final AtomicReference<Transaction> foundTransaction = new AtomicReference<>();
final BlocksDownloadedEventListener listener = (peer, block, filteredBlock, blocksLeft) -> { final BlocksDownloadedEventListener listener = (peer, block, filteredBlock, blocksLeft) -> {
List<Transaction> transactions = block.getTransactions(); List<Transaction> transactions = block.getTransactions();
@ -498,7 +487,7 @@ public class BTC {
for (Transaction transaction : transactions) for (Transaction transaction : transactions)
if (transaction.getTxId().equals(txHash)) { if (transaction.getTxId().equals(txHash)) {
System.out.println(String.format("We downloaded block containing tx!")); System.out.println(String.format("We downloaded block containing tx!"));
transactionStorage.store(transaction); foundTransaction.set(transaction);
} }
}; };
@ -508,7 +497,7 @@ public class BTC {
try { try {
replayChain(startTime, wallet, replayHooks); replayChain(startTime, wallet, replayHooks);
Transaction realTx = transactionStorage.getTransaction(); Transaction realTx = foundTransaction.get();
return realTx.getOutputs(); return realTx.getOutputs();
} catch (BlockStoreException e) { } catch (BlockStoreException e) {
LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage())); LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage()));

View File

@ -16,9 +16,11 @@ import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptChunk; import org.bitcoinj.script.ScriptChunk;
import org.bitcoinj.script.ScriptOpCodes; import org.bitcoinj.script.ScriptOpCodes;
import org.ciyam.at.API;
import org.ciyam.at.FunctionCode; import org.ciyam.at.FunctionCode;
import org.ciyam.at.MachineState; import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode; import org.ciyam.at.OpCode;
import org.qortal.utils.Base58;
import org.qortal.utils.BitTwiddling; import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode; import com.google.common.hash.HashCode;
@ -170,9 +172,10 @@ public class BTCACCT {
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, null, redeemSigScriptBuilder); return buildP2shTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, null, redeemSigScriptBuilder);
} }
public static byte[] buildQortalAT(byte[] secretHash, String destinationQortalAddress, long refundMinutes, BigDecimal initialPayout) { public static byte[] buildQortalAT(byte[] secretHash, String recipientQortalAddress, long refundMinutes, BigDecimal initialPayout) {
// Labels for data segment addresses // Labels for data segment addresses
int addrCounter = 0; int addrCounter = 0;
// Constants (with corresponding dataByteBuffer.put*() calls below)
final int addrHashPart1 = addrCounter++; final int addrHashPart1 = addrCounter++;
final int addrHashPart2 = addrCounter++; final int addrHashPart2 = addrCounter++;
final int addrHashPart3 = addrCounter++; final int addrHashPart3 = addrCounter++;
@ -185,6 +188,8 @@ public class BTCACCT {
final int addrHashTempIndex = addrCounter++; final int addrHashTempIndex = addrCounter++;
final int addrHashTempLength = addrCounter++; final int addrHashTempLength = addrCounter++;
final int addrInitialPayoutAmount = addrCounter++; final int addrInitialPayoutAmount = addrCounter++;
final int addrExpectedTxType = addrCounter++;
// Variables
final int addrRefundTimestamp = addrCounter++; final int addrRefundTimestamp = addrCounter++;
final int addrLastTimestamp = addrCounter++; final int addrLastTimestamp = addrCounter++;
final int addrBlockTimestamp = addrCounter++; final int addrBlockTimestamp = addrCounter++;
@ -200,72 +205,99 @@ public class BTCACCT {
final int addrHashTemp4 = addrCounter++; final int addrHashTemp4 = addrCounter++;
// Data segment // Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * 8); ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
// Hash of secret into HashPart1-4 // Hash of secret into HashPart1-4
dataByteBuffer.put(secretHash); assert dataByteBuffer.position() == addrHashPart1 * MachineState.VALUE_SIZE : "addrHashPart1 incorrect";
dataByteBuffer.put(Bytes.ensureCapacity(secretHash, 32, 0));
// Destination Qortal account's public key // Recipient Qortal address, decoded from Base58
dataByteBuffer.put(Bytes.ensureCapacity(destinationQortalAddress.getBytes(), 32, 0)); assert dataByteBuffer.position() == addrAddressPart1 * MachineState.VALUE_SIZE : "addrAddressPart1 incorrect";
byte[] recipientAddressBytes = Base58.decode(recipientQortalAddress);
dataByteBuffer.put(Bytes.ensureCapacity(recipientAddressBytes, 32, 0));
// Expiry in minutes // Expiry in minutes
assert dataByteBuffer.position() == addrRefundMinutes * MachineState.VALUE_SIZE : "addrRefundMinutes incorrect";
dataByteBuffer.putLong(refundMinutes); dataByteBuffer.putLong(refundMinutes);
// Temp buffer for hashing any passed secret // Source location and length for hashing any passed secret
assert dataByteBuffer.position() == addrHashTempIndex * MachineState.VALUE_SIZE : "addrHashTempIndex incorrect";
dataByteBuffer.putLong(addrHashTemp1); dataByteBuffer.putLong(addrHashTemp1);
assert dataByteBuffer.position() == addrHashTempLength * MachineState.VALUE_SIZE : "addrHashTempLength incorrect";
dataByteBuffer.putLong(32L); dataByteBuffer.putLong(32L);
// Initial payout amount // Initial payout amount
assert dataByteBuffer.position() == addrInitialPayoutAmount * MachineState.VALUE_SIZE : "addrInitialPayoutAmount incorrect";
dataByteBuffer.putLong(initialPayout.unscaledValue().longValue()); 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);
// Code labels // Code labels
final int addrTxLoop = 0x36; final int addrTxLoop = 0x0036;
final int addrCheckTx = 0x4b; final int addrCheckTx = 0x004b;
final int addrCheckSender = 0x64; final int addrRefund = 0x00c6;
final int addrCheckMessage = 0xab; final int addrEndOfCode = 0x00cd;
final int addrPayout = 0xdf;
final int addrRefund = 0x102;
final int addrEndOfCode = 0x109;
int tempPC; int tempPC;
ByteBuffer codeByteBuffer = ByteBuffer.allocate(addrEndOfCode * 1); ByteBuffer codeByteBuffer = ByteBuffer.allocate(addrEndOfCode * 1);
// init: /* Initialization */
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_CREATION_TIMESTAMP.value).putInt(addrRefundTimestamp);
codeByteBuffer.put(OpCode.SET_DAT.value).putInt(addrLastTimestamp).putInt(addrRefundTimestamp);
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(FunctionCode.ADD_MINUTES_TO_TIMESTAMP.value).putInt(addrRefundTimestamp)
.putInt(addrRefundTimestamp).putInt(addrRefundMinutes);
// 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);
// Load recipient's address into B register
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrAddressPart1); 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); codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PAY_TO_ADDRESS_IN_B.value).putInt(addrInitialPayoutAmount);
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.value); codeByteBuffer.put(OpCode.SET_PCS.value);
// loop: /* Main loop */
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_BLOCK_TIMESTAMP.value).putInt(addrBlockTimestamp);
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BLT_DAT.value).putInt(addrBlockTimestamp).putInt(addrRefundTimestamp).put((byte) (addrTxLoop - tempPC));
codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(addrRefund);
// txloop: // 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));
/* Transaction processing loop */
assert codeByteBuffer.position() == addrTxLoop : "addrTxLoop incorrect"; assert codeByteBuffer.position() == addrTxLoop : "addrTxLoop incorrect";
// 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); 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); 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(); tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(addrComparator).put((byte) (addrCheckTx - tempPC)); codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(addrComparator).put((byte) (addrCheckTx - tempPC));
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.value); codeByteBuffer.put(OpCode.STP_IMD.value);
// checkTx: /* Check transaction */
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A.value).putInt(addrLastTimestamp); assert codeByteBuffer.position() == addrCheckTx : "addrCheckTx incorrect";
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TYPE_FROM_TX_IN_A.value).putInt(addrTxType);
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BNZ_DAT.value).putInt(addrTxType).put((byte) (addrCheckSender - tempPC));
codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(addrTxLoop);
// checkSender // Update our 'last found transaction's timestamp' using 'timestamp' from transaction
assert codeByteBuffer.position() == addrCheckSender : "addrCheckSender incorrect"; 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's sender */
// 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); 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); 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(); tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp1).putInt(addrAddressPart1).put((byte) (addrTxLoop - tempPC)); codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp1).putInt(addrAddressPart1).put((byte) (addrTxLoop - tempPC));
tempPC = codeByteBuffer.position(); tempPC = codeByteBuffer.position();
@ -275,26 +307,38 @@ public class BTCACCT {
tempPC = codeByteBuffer.position(); tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp4).putInt(addrAddressPart4).put((byte) (addrTxLoop - tempPC)); codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp4).putInt(addrAddressPart4).put((byte) (addrTxLoop - tempPC));
// checkMessage: /* Check 'secret' in transaction's message */
assert codeByteBuffer.position() == addrCheckMessage : "addrCheckMessage incorrect";
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B.value);
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B_IND.value).putInt(addrHashTemp1);
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrHashPart1);
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_HASH160_WITH_B.value).putInt(addrHashTempIndex).putInt(addrHashTempLength);
tempPC = codeByteBuffer.position();
codeByteBuffer.put(OpCode.BNZ_DAT.value).putInt(addrComparator).put((byte) (addrPayout - tempPC));
codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(addrTxLoop);
// payout: // Extract message from transaction into B register
assert codeByteBuffer.position() == addrPayout : "addrPayout incorrect"; 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));
/* Success! Pay balance to intended recipient */
// Load B register with intended recipient address.
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrAddressPart1); 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); 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); codeByteBuffer.put(OpCode.FIN_IMD.value);
// refund: /* Refund balance back to AT creator */
assert codeByteBuffer.position() == addrRefund : "addrRefund incorrect"; assert codeByteBuffer.position() == addrRefund : "addrRefund incorrect";
// Load B register with AT creator's address.
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_CREATOR_INTO_B.value); 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); 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); codeByteBuffer.put(OpCode.FIN_IMD.value);
// end-of-code // end-of-code

View File

@ -108,14 +108,15 @@ public class Crypto {
return isValidTypedAddress(address, ADDRESS_VERSION, AT_ADDRESS_VERSION); return isValidTypedAddress(address, ADDRESS_VERSION, AT_ADDRESS_VERSION);
} }
public static boolean isValidAddress(byte[] addressBytes) {
return areValidTypedAddressBytes(addressBytes, ADDRESS_VERSION, AT_ADDRESS_VERSION);
}
public static boolean isValidAtAddress(String address) { public static boolean isValidAtAddress(String address) {
return isValidTypedAddress(address, AT_ADDRESS_VERSION); return isValidTypedAddress(address, AT_ADDRESS_VERSION);
} }
private static boolean isValidTypedAddress(String address, byte...addressVersions) { private static boolean isValidTypedAddress(String address, byte...addressVersions) {
if (addressVersions == null || addressVersions.length == 0)
return false;
byte[] addressBytes; byte[] addressBytes;
try { try {
@ -125,6 +126,13 @@ public class Crypto {
return false; return false;
} }
return areValidTypedAddressBytes(addressBytes, addressVersions);
}
private static boolean areValidTypedAddressBytes(byte[] addressBytes, byte...addressVersions) {
if (addressVersions == null || addressVersions.length == 0)
return false;
// Check address length // Check address length
if (addressBytes == null || addressBytes.length != Account.ADDRESS_LENGTH) if (addressBytes == null || addressBytes.length != Account.ADDRESS_LENGTH)
return false; return false;

View File

@ -46,6 +46,14 @@ public class ATData {
this.frozenBalance = BigDecimal.valueOf(frozenBalance, 8); this.frozenBalance = BigDecimal.valueOf(frozenBalance, 8);
} }
/** For constructing skeleton ATData with bare minimum info. */
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, long assetId) {
this.ATAddress = ATAddress;
this.creatorPublicKey = creatorPublicKey;
this.creation = creation;
this.assetId = assetId;
}
// Getters / setters // Getters / setters
public String getATAddress() { public String getATAddress() {

View File

@ -8,12 +8,15 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.ciyam.at.MachineState; import org.ciyam.at.MachineState;
import org.ciyam.at.Timestamp;
import org.qortal.account.Account; import org.qortal.account.Account;
import org.qortal.asset.Asset; import org.qortal.asset.Asset;
import org.qortal.at.AT; import org.qortal.at.AT;
import org.qortal.at.QortalATAPI;
import org.qortal.block.BlockChain; import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto; import org.qortal.crypto.Crypto;
import org.qortal.data.asset.AssetData; import org.qortal.data.asset.AssetData;
import org.qortal.data.at.ATData;
import org.qortal.data.transaction.DeployAtTransactionData; import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.TransactionData; import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException; import org.qortal.repository.DataException;
@ -79,6 +82,7 @@ public class DeployAtTransaction extends Transaction {
/** Returns AT version from the header bytes */ /** Returns AT version from the header bytes */
private short getVersion() { private short getVersion() {
byte[] creationBytes = deployATTransactionData.getCreationBytes(); byte[] creationBytes = deployATTransactionData.getCreationBytes();
// XXX this is currently little-endian, but ok for the moment as newer versions will be reported as 512+ so tests like version >= 2 will work
return (short) ((creationBytes[0] & 0xff) | (creationBytes[1] << 8)); // Little-endian return (short) ((creationBytes[0] & 0xff) | (creationBytes[1] << 8)); // Little-endian
} }
@ -87,6 +91,13 @@ public class DeployAtTransaction extends Transaction {
if (this.deployATTransactionData.getAtAddress() != null) if (this.deployATTransactionData.getAtAddress() != null)
return; return;
// For new version, simply use transaction signature
if (this.getVersion() > 1) {
String atAddress = Crypto.toATAddress(this.deployATTransactionData.getSignature());
this.deployATTransactionData.setAtAddress(atAddress);
return;
}
int blockHeight = this.getHeight(); int blockHeight = this.getHeight();
if (blockHeight == 0) if (blockHeight == 0)
blockHeight = this.repository.getBlockRepository().getBlockchainHeight() + 1; blockHeight = this.repository.getBlockRepository().getBlockchainHeight() + 1;
@ -189,8 +200,21 @@ public class DeployAtTransaction extends Transaction {
// Check creation bytes are valid (for v2+) // Check creation bytes are valid (for v2+)
if (this.getVersion() >= 2) { if (this.getVersion() >= 2) {
// Do actual validation // Do actual validation
ensureATAddress();
// Just enough AT data to allow API to query initial balances, etc.
String atAddress = this.deployATTransactionData.getAtAddress();
byte[] creatorPublicKey = this.deployATTransactionData.getCreatorPublicKey();
long creation = this.deployATTransactionData.getTimestamp();
ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId);
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
long blockTimestamp = Timestamp.toLong(height, 0);
QortalATAPI api = new QortalATAPI(repository, skeletonAtData, blockTimestamp);
try { try {
new MachineState(deployATTransactionData.getCreationBytes()); new MachineState(api, deployATTransactionData.getCreationBytes());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
// Not valid // Not valid
return ValidationResult.INVALID_CREATION_BYTES; return ValidationResult.INVALID_CREATION_BYTES;

View File

@ -129,8 +129,13 @@ public class DeployAT {
} }
BigDecimal fee = BigDecimal.ZERO; BigDecimal fee = BigDecimal.ZERO;
String name = "QORT-BTC cross-chain trade";
String description = String.format("Qortal-Bitcoin cross-chain trade between %s and %s", refundAccount.getAddress(), redeemAddress);
String atType = "ACCT";
String tags = "QORT-BTC ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, refundAccount.getPublicKey(), fee, null); BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, refundAccount.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, "QORT-BTC", "QORT-BTC ACCT", "", "", creationBytes, qortAmount, Asset.QORT); TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, qortAmount, Asset.QORT);
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);

View File

@ -22,32 +22,32 @@ public class GetTransaction {
if (error != null) if (error != null)
System.err.println(error); System.err.println(error);
System.err.println(String.format("usage: GetTransaction <bitcoin-tx>")); System.err.println(String.format("usage: GetTransaction <bitcoin-tx> <start-time>"));
System.err.println(String.format("example (mainnet): GetTransaction 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660")); System.err.println(String.format("example (mainnet): GetTransaction 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660 1585317000"));
System.err.println(String.format("example (testnet): GetTransaction 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e")); System.err.println(String.format("example (testnet): GetTransaction 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e 1584376000"));
System.exit(1); System.exit(1);
} }
public static void main(String[] args) { public static void main(String[] args) {
if (args.length < 1 || args.length > 1) if (args.length < 2 || args.length > 2)
usage(null); usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0); Security.insertProviderAt(new BouncyCastleProvider(), 0);
Settings.fileInstance("settings-test.json"); Settings.fileInstance("settings-test.json");
byte[] transactionId = null; byte[] transactionId = null;
int startTime = 0;
try { try {
int argIndex = 0; int argIndex = 0;
transactionId = HashCode.fromString(args[argIndex++]).asBytes(); transactionId = HashCode.fromString(args[argIndex++]).asBytes();
startTime = Integer.parseInt(args[argIndex++]);
} catch (NumberFormatException | AddressFormatException e) { } catch (NumberFormatException | AddressFormatException e) {
usage(String.format("Argument format exception: %s", e.getMessage())); usage(String.format("Argument format exception: %s", e.getMessage()));
} }
// Chain replay start time
long startTime = (System.currentTimeMillis() / 1000L) - 14 * 24 * 60 * 60; // 14 days before now, in seconds
// Grab all outputs from transaction // Grab all outputs from transaction
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(transactionId, startTime); List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(transactionId, startTime);
if (fundingOutputs == null) { if (fundingOutputs == null) {