diff --git a/pom.xml b/pom.xml index 15509805..83cb4b4d 100644 --- a/pom.xml +++ b/pom.xml @@ -406,7 +406,7 @@ org.ciyam at - 1.3 + 1.3.2 diff --git a/src/main/java/org/qortal/at/AT.java b/src/main/java/org/qortal/at/AT.java index a59970b3..23bf02cd 100644 --- a/src/main/java/org/qortal/at/AT.java +++ b/src/main/java/org/qortal/at/AT.java @@ -5,6 +5,7 @@ import java.nio.ByteBuffer; import java.util.List; import org.ciyam.at.MachineState; +import org.ciyam.at.Timestamp; import org.qortal.asset.Asset; import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; @@ -48,7 +49,13 @@ public class AT { short version = (short) ((creationBytes[0] & 0xff) | (creationBytes[1] << 8)); // Little-endian 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(), machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(), diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java index 9016bb17..27f67ac0 100644 --- a/src/main/java/org/qortal/at/QortalATAPI.java +++ b/src/main/java/org/qortal/at/QortalATAPI.java @@ -4,6 +4,7 @@ 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 org.ciyam.at.API; @@ -35,11 +36,15 @@ import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.transaction.AtTransaction; import org.qortal.transaction.Transaction; +import org.qortal.transaction.Transaction.TransactionType; +import org.qortal.utils.Base58; import com.google.common.primitives.Bytes; public class QortalATAPI extends API { + private static final byte[] ADDRESS_PADDING = new byte[32 - Account.ADDRESS_LENGTH]; + // Properties private Repository repository; private ATData atData; @@ -316,18 +321,30 @@ public class QortalATAPI extends API { public void putAddressFromTransactionInAIntoB(MachineState state) { TransactionData transactionData = this.getTransactionFromA(state); - // We actually use public key as it has more potential utility (e.g. message verification) than an address - byte[] bytes = transactionData.getCreatorPublicKey(); + String address; + 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 public void putCreatorAddressIntoB(MachineState state) { - // We actually use public key as it has more potential utility (e.g. message verification) than an address - byte[] bytes = atData.getCreatorPublicKey(); + byte[] publicKey = atData.getCreatorPublicKey(); + String address = Crypto.toAddress(publicKey); + byte[] addressBytes = Bytes.ensureCapacity(address.getBytes(), 32, 0); - this.setB(state, bytes); + this.setB(state, addressBytes); } @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. */ - - // XXX THE ABOVE IS NO LONGER TRUE IN QORTAL! - // return this.blockTimestamp + this.transactions.size(); - throw new RuntimeException("AT timestamp code not fixed!"); + return this.blockTimestamp + this.transactions.size(); } /** 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. *

- * 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,
+ * and bytes 26 to 32 are zero, then use as an address, but only if valid. *

* Otherwise, assume B is a public key. - * @return */ private Account getAccountFromB(MachineState state) { byte[] bBytes = state.getB(); - if (bBytes[0] == 'Q') { - int zeroIndex = Bytes.indexOf(bBytes, (byte) 0); - if (zeroIndex > 0) { - String address = new String(bBytes, 0, zeroIndex); - if (Crypto.isValidAddress(address)) - return new Account(this.repository, address); - } + 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) { + // Extract only the bytes containing address + byte[] addressBytes = Arrays.copyOf(bBytes, Account.ADDRESS_LENGTH); + // If address (in byte form) is valid... + 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); diff --git a/src/main/java/org/qortal/crosschain/BTC.java b/src/main/java/org/qortal/crosschain/BTC.java index f7f9a979..2ad069f7 100644 --- a/src/main/java/org/qortal/crosschain/BTC.java +++ b/src/main/java/org/qortal/crosschain/BTC.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicReference; import org.apache.logging.log4j.LogManager; 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 getOutputs(byte[] txId, long startTime) { Wallet wallet = createEmptyWallet(); @@ -487,7 +476,7 @@ public class BTC { final Sha256Hash txHash = Sha256Hash.wrap(txId); - final TransactionStorage transactionStorage = new TransactionStorage(); + final AtomicReference foundTransaction = new AtomicReference<>(); final BlocksDownloadedEventListener listener = (peer, block, filteredBlock, blocksLeft) -> { List transactions = block.getTransactions(); @@ -498,7 +487,7 @@ public class BTC { for (Transaction transaction : transactions) if (transaction.getTxId().equals(txHash)) { System.out.println(String.format("We downloaded block containing tx!")); - transactionStorage.store(transaction); + foundTransaction.set(transaction); } }; @@ -508,7 +497,7 @@ public class BTC { try { replayChain(startTime, wallet, replayHooks); - Transaction realTx = transactionStorage.getTransaction(); + Transaction realTx = foundTransaction.get(); return realTx.getOutputs(); } catch (BlockStoreException e) { LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage())); diff --git a/src/main/java/org/qortal/crosschain/BTCACCT.java b/src/main/java/org/qortal/crosschain/BTCACCT.java index a0480317..1ee78f2c 100644 --- a/src/main/java/org/qortal/crosschain/BTCACCT.java +++ b/src/main/java/org/qortal/crosschain/BTCACCT.java @@ -16,9 +16,11 @@ import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptChunk; import org.bitcoinj.script.ScriptOpCodes; +import org.ciyam.at.API; import org.ciyam.at.FunctionCode; import org.ciyam.at.MachineState; import org.ciyam.at.OpCode; +import org.qortal.utils.Base58; import org.qortal.utils.BitTwiddling; import com.google.common.hash.HashCode; @@ -170,9 +172,10 @@ public class BTCACCT { 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 int addrCounter = 0; + // Constants (with corresponding dataByteBuffer.put*() calls below) final int addrHashPart1 = addrCounter++; final int addrHashPart2 = addrCounter++; final int addrHashPart3 = addrCounter++; @@ -185,6 +188,8 @@ public class BTCACCT { final int addrHashTempIndex = addrCounter++; final int addrHashTempLength = addrCounter++; final int addrInitialPayoutAmount = addrCounter++; + final int addrExpectedTxType = addrCounter++; + // Variables final int addrRefundTimestamp = addrCounter++; final int addrLastTimestamp = addrCounter++; final int addrBlockTimestamp = addrCounter++; @@ -200,72 +205,99 @@ public class BTCACCT { final int addrHashTemp4 = addrCounter++; // Data segment - ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * 8); + ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE); // 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 - dataByteBuffer.put(Bytes.ensureCapacity(destinationQortalAddress.getBytes(), 32, 0)); + // Recipient Qortal address, decoded from Base58 + assert dataByteBuffer.position() == addrAddressPart1 * MachineState.VALUE_SIZE : "addrAddressPart1 incorrect"; + byte[] recipientAddressBytes = Base58.decode(recipientQortalAddress); + dataByteBuffer.put(Bytes.ensureCapacity(recipientAddressBytes, 32, 0)); // Expiry in minutes + assert dataByteBuffer.position() == addrRefundMinutes * MachineState.VALUE_SIZE : "addrRefundMinutes incorrect"; 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); + 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); + // Code labels - final int addrTxLoop = 0x36; - final int addrCheckTx = 0x4b; - final int addrCheckSender = 0x64; - final int addrCheckMessage = 0xab; - final int addrPayout = 0xdf; - final int addrRefund = 0x102; - final int addrEndOfCode = 0x109; + final int addrTxLoop = 0x0036; + final int addrCheckTx = 0x004b; + final int addrRefund = 0x00c6; + final int addrEndOfCode = 0x00cd; int tempPC; ByteBuffer codeByteBuffer = ByteBuffer.allocate(addrEndOfCode * 1); - // init: - 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); + /* Initialization */ + // 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); + // 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); + // Set restart position to after this opcode codeByteBuffer.put(OpCode.SET_PCS.value); - // 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); + /* Main loop */ - // 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"; + + // 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); - // checkTx: - codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A.value).putInt(addrLastTimestamp); - 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); + /* Check transaction */ + assert codeByteBuffer.position() == addrCheckTx : "addrCheckTx incorrect"; - // checkSender - assert codeByteBuffer.position() == addrCheckSender : "addrCheckSender incorrect"; + // 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'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); + // 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(); @@ -275,26 +307,38 @@ public class BTCACCT { tempPC = codeByteBuffer.position(); codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp4).putInt(addrAddressPart4).put((byte) (addrTxLoop - tempPC)); - // checkMessage: - 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); + /* Check 'secret' in transaction's message */ - // payout: - assert codeByteBuffer.position() == addrPayout : "addrPayout incorrect"; + // 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)); + + /* 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); + // 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); - // refund: + /* Refund balance back to AT creator */ 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); + // 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); // end-of-code diff --git a/src/main/java/org/qortal/crypto/Crypto.java b/src/main/java/org/qortal/crypto/Crypto.java index 36c06585..c940c6ab 100644 --- a/src/main/java/org/qortal/crypto/Crypto.java +++ b/src/main/java/org/qortal/crypto/Crypto.java @@ -108,14 +108,15 @@ public class Crypto { 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) { return isValidTypedAddress(address, AT_ADDRESS_VERSION); } private static boolean isValidTypedAddress(String address, byte...addressVersions) { - if (addressVersions == null || addressVersions.length == 0) - return false; - byte[] addressBytes; try { @@ -125,6 +126,13 @@ public class Crypto { 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 if (addressBytes == null || addressBytes.length != Account.ADDRESS_LENGTH) return false; diff --git a/src/main/java/org/qortal/data/at/ATData.java b/src/main/java/org/qortal/data/at/ATData.java index 0ab73bdc..b12439e0 100644 --- a/src/main/java/org/qortal/data/at/ATData.java +++ b/src/main/java/org/qortal/data/at/ATData.java @@ -46,6 +46,14 @@ public class ATData { 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 public String getATAddress() { diff --git a/src/main/java/org/qortal/transaction/DeployAtTransaction.java b/src/main/java/org/qortal/transaction/DeployAtTransaction.java index 191adeec..8995b626 100644 --- a/src/main/java/org/qortal/transaction/DeployAtTransaction.java +++ b/src/main/java/org/qortal/transaction/DeployAtTransaction.java @@ -8,12 +8,15 @@ import java.util.ArrayList; import java.util.List; import org.ciyam.at.MachineState; +import org.ciyam.at.Timestamp; import org.qortal.account.Account; import org.qortal.asset.Asset; import org.qortal.at.AT; +import org.qortal.at.QortalATAPI; import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.asset.AssetData; +import org.qortal.data.at.ATData; import org.qortal.data.transaction.DeployAtTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.repository.DataException; @@ -79,6 +82,7 @@ public class DeployAtTransaction extends Transaction { /** Returns AT version from the header bytes */ private short getVersion() { 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 } @@ -87,6 +91,13 @@ public class DeployAtTransaction extends Transaction { if (this.deployATTransactionData.getAtAddress() != null) 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(); if (blockHeight == 0) blockHeight = this.repository.getBlockRepository().getBlockchainHeight() + 1; @@ -189,8 +200,21 @@ public class DeployAtTransaction extends Transaction { // Check creation bytes are valid (for v2+) if (this.getVersion() >= 2) { // 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 { - new MachineState(deployATTransactionData.getCreationBytes()); + new MachineState(api, deployATTransactionData.getCreationBytes()); } catch (IllegalArgumentException e) { // Not valid return ValidationResult.INVALID_CREATION_BYTES; diff --git a/src/test/java/org/qortal/test/btcacct/DeployAT.java b/src/test/java/org/qortal/test/btcacct/DeployAT.java index 36dbdc5a..98e2daa7 100644 --- a/src/test/java/org/qortal/test/btcacct/DeployAT.java +++ b/src/test/java/org/qortal/test/btcacct/DeployAT.java @@ -129,8 +129,13 @@ public class DeployAT { } 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); - 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); diff --git a/src/test/java/org/qortal/test/btcacct/GetTransaction.java b/src/test/java/org/qortal/test/btcacct/GetTransaction.java index c97762b9..48af4ebf 100644 --- a/src/test/java/org/qortal/test/btcacct/GetTransaction.java +++ b/src/test/java/org/qortal/test/btcacct/GetTransaction.java @@ -22,32 +22,32 @@ public class GetTransaction { if (error != null) System.err.println(error); - System.err.println(String.format("usage: GetTransaction ")); - System.err.println(String.format("example (mainnet): GetTransaction 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660")); - System.err.println(String.format("example (testnet): GetTransaction 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e")); + System.err.println(String.format("usage: GetTransaction ")); + System.err.println(String.format("example (mainnet): GetTransaction 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660 1585317000")); + System.err.println(String.format("example (testnet): GetTransaction 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e 1584376000")); System.exit(1); } public static void main(String[] args) { - if (args.length < 1 || args.length > 1) + if (args.length < 2 || args.length > 2) usage(null); Security.insertProviderAt(new BouncyCastleProvider(), 0); Settings.fileInstance("settings-test.json"); byte[] transactionId = null; + int startTime = 0; try { int argIndex = 0; transactionId = HashCode.fromString(args[argIndex++]).asBytes(); + + startTime = Integer.parseInt(args[argIndex++]); } catch (NumberFormatException | AddressFormatException e) { 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 List fundingOutputs = BTC.getInstance().getOutputs(transactionId, startTime); if (fundingOutputs == null) {