diff --git a/.settings/org.eclipse.jdt.apt.core.prefs b/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 00000000..2ce97d10 --- /dev/null +++ b/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations diff --git a/src/Start.java b/src/Start.java index cbab3830..637899a6 100644 --- a/src/Start.java +++ b/src/Start.java @@ -1,5 +1,4 @@ -import api.ApiClient; import api.ApiService; import repository.DataException; import repository.RepositoryFactory; @@ -22,4 +21,5 @@ public class Start { //String test = client.executeCommand("GET blocks/first"); //System.out.println(test); } + } diff --git a/src/blockgenerator.java b/src/blockgenerator.java index 700482bd..eec6a18d 100644 --- a/src/blockgenerator.java +++ b/src/blockgenerator.java @@ -3,12 +3,9 @@ import java.security.SecureRandom; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import qora.account.PrivateKeyAccount; import qora.block.BlockChain; import qora.block.BlockGenerator; import repository.DataException; -import repository.Repository; -import repository.RepositoryManager; import utils.Base58; public class blockgenerator { diff --git a/src/crosschain/BTC.java b/src/crosschain/BTC.java new file mode 100644 index 00000000..f3cf0286 --- /dev/null +++ b/src/crosschain/BTC.java @@ -0,0 +1,304 @@ +package crosschain; + +import java.io.ByteArrayInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.util.Date; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.BlockChain; +import org.bitcoinj.core.CheckpointManager; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.PeerGroup; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.StoredBlock; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.core.Utils; +import org.bitcoinj.core.VerificationException; +import org.bitcoinj.core.listeners.NewBestBlockListener; +import org.bitcoinj.net.discovery.DnsDiscovery; +import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.TestNet3Params; +import org.bitcoinj.script.Script; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.store.SPVBlockStore; +import org.bitcoinj.utils.Threading; +import org.bitcoinj.wallet.KeyChainGroup; +import org.bitcoinj.wallet.Wallet; +import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener; + +import settings.Settings; + +public class BTC { + + private static class RollbackBlockChain extends BlockChain { + + public RollbackBlockChain(NetworkParameters params, BlockStore blockStore) throws BlockStoreException { + super(params, blockStore); + } + + @Override + public void setChainHead(StoredBlock chainHead) throws BlockStoreException { + super.setChainHead(chainHead); + } + + } + + private static class UpdateableCheckpointManager extends CheckpointManager implements NewBestBlockListener { + + private static final int checkpointInterval = 500; + + private static final String minimalTestNet3TextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO\n"; + private static final String minimalMainNetTextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAABjl7tqvU/FIcDT9gcbVlA4nwtFUbxAtOawZzBpAAAAAKzkcK7NqciBjI/ldojNKncrWleVSgDfBCCn3VRrbSxXaw5/Sf//AB0z8Bkv\n"; + + public UpdateableCheckpointManager(NetworkParameters params) throws IOException { + super(params, getMinimalTextFileStream(params)); + } + + public UpdateableCheckpointManager(NetworkParameters params, InputStream inputStream) throws IOException { + super(params, inputStream); + } + + private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params) { + if (params == MainNetParams.get()) + return new ByteArrayInputStream(minimalMainNetTextFile.getBytes()); + + if (params == TestNet3Params.get()) + return new ByteArrayInputStream(minimalTestNet3TextFile.getBytes()); + + throw new RuntimeException("Failed to construct empty UpdateableCheckpointManageer"); + } + + @Override + public void notifyNewBestBlock(StoredBlock block) throws VerificationException { + int height = block.getHeight(); + + if (height % checkpointInterval == 0) + checkpoints.put(block.getHeader().getTimeSeconds(), block); + } + + public void saveAsText(File textFile) throws FileNotFoundException { + try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(textFile), StandardCharsets.US_ASCII))) { + writer.println("TXT CHECKPOINTS 1"); + writer.println("0"); // Number of signatures to read. Do this later. + writer.println(checkpoints.size()); + ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); + for (StoredBlock block : checkpoints.values()) { + block.serializeCompact(buffer); + writer.println(CheckpointManager.BASE64.encode(buffer.array())); + buffer.position(0); + } + } + } + + public void saveAsBinary(File file) throws IOException { + try (final FileOutputStream fileOutputStream = new FileOutputStream(file, false)) { + MessageDigest digest = Sha256Hash.newDigest(); + + try (final DigestOutputStream digestOutputStream = new DigestOutputStream(fileOutputStream, digest)) { + digestOutputStream.on(false); + + try (final DataOutputStream dataOutputStream = new DataOutputStream(digestOutputStream)) { + dataOutputStream.writeBytes("CHECKPOINTS 1"); + dataOutputStream.writeInt(0); // Number of signatures to read. Do this later. + digestOutputStream.on(true); + dataOutputStream.writeInt(checkpoints.size()); + ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); + for (StoredBlock block : checkpoints.values()) { + block.serializeCompact(buffer); + dataOutputStream.write(buffer.array()); + buffer.position(0); + } + } + } + } + } + + } + + private static BTC instance; + private static final Object instanceLock = new Object(); + + private static File directory; + private static String chainFileName; + private static String checkpointsFileName; + + private static NetworkParameters params; + private static PeerGroup peerGroup; + private static BlockStore blockStore; + private static RollbackBlockChain chain; + private static UpdateableCheckpointManager manager; + + private BTC() { + // Start wallet + if (Settings.getInstance().isTestNet()) { + params = TestNet3Params.get(); + chainFileName = "bitcoinj-testnet.spvchain"; + checkpointsFileName = "checkpoints-testnet.txt"; + } else { + params = MainNetParams.get(); + chainFileName = "bitcoinj.spvchain"; + checkpointsFileName = "checkpoints.txt"; + } + + directory = new File("Qora-BTC"); + if (!directory.exists()) + directory.mkdirs(); + + File chainFile = new File(directory, chainFileName); + + try { + blockStore = new SPVBlockStore(params, chainFile); + } catch (BlockStoreException e) { + throw new RuntimeException("Failed to open/create BTC SPVBlockStore", e); + } + + File checkpointsFile = new File(directory, checkpointsFileName); + try (InputStream checkpointsStream = new FileInputStream(checkpointsFile)) { + manager = new UpdateableCheckpointManager(params, checkpointsStream); + } catch (FileNotFoundException e) { + // Construct with no checkpoints then + try { + manager = new UpdateableCheckpointManager(params); + } catch (IOException e2) { + throw new RuntimeException("Failed to create new BTC checkpoints", e2); + } + } catch (IOException e) { + throw new RuntimeException("Failed to load BTC checkpoints", e); + } + + try { + chain = new RollbackBlockChain(params, blockStore); + } catch (BlockStoreException e) { + throw new RuntimeException("Failed to construct BTC blockchain", e); + } + + peerGroup = new PeerGroup(params, chain); + peerGroup.setUserAgent("qqq", "1.0"); + peerGroup.addPeerDiscovery(new DnsDiscovery(params)); + peerGroup.start(); + } + + public static BTC getInstance() { + if (instance == null) + synchronized (instanceLock) { + if (instance == null) + instance = new BTC(); + } + + return instance; + } + + public void shutdown() { + synchronized (instanceLock) { + if (instance == null) + return; + + instance = null; + } + + peerGroup.stop(); + + try { + blockStore.close(); + } catch (BlockStoreException e) { + // What can we do? + } + } + + protected Wallet createEmptyWallet() { + ECKey dummyKey = new ECKey(); + + KeyChainGroup keyChainGroup = new KeyChainGroup(params); + keyChainGroup.importKeys(dummyKey); + + Wallet wallet = new Wallet(params, keyChainGroup); + + wallet.removeKey(dummyKey); + + return wallet; + } + + public void watch(String base58Address, long startTime) throws InterruptedException, ExecutionException, TimeoutException, BlockStoreException { + Wallet wallet = createEmptyWallet(); + + WalletCoinsReceivedEventListener coinsReceivedListener = new WalletCoinsReceivedEventListener() { + @Override + public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) { + System.out.println("Coins received via transaction " + tx.getHashAsString()); + } + }; + wallet.addCoinsReceivedEventListener(coinsReceivedListener); + + Address address = Address.fromBase58(params, base58Address); + wallet.addWatchedAddress(address, startTime); + + StoredBlock checkpoint = manager.getCheckpointBefore(startTime); + blockStore.put(checkpoint); + blockStore.setChainHead(checkpoint); + chain.setChainHead(checkpoint); + + chain.addWallet(wallet); + peerGroup.addWallet(wallet); + peerGroup.setFastCatchupTimeSecs(startTime); + + System.out.println("Starting download..."); + peerGroup.downloadBlockChain(); + + List outputs = wallet.getWatchedOutputs(true); + + peerGroup.removeWallet(wallet); + chain.removeWallet(wallet); + + for (TransactionOutput output : outputs) + System.out.println(output.toString()); + } + + public void watch(Script script) { + // wallet.addWatchedScripts(scripts); + } + + public void updateCheckpoints() { + final long now = new Date().getTime() / 1000; + + try { + StoredBlock checkpoint = manager.getCheckpointBefore(now); + blockStore.put(checkpoint); + blockStore.setChainHead(checkpoint); + chain.setChainHead(checkpoint); + } catch (BlockStoreException e) { + throw new RuntimeException("Failed to update BTC checkpoints", e); + } + + peerGroup.setFastCatchupTimeSecs(now); + + chain.addNewBestBlockListener(Threading.SAME_THREAD, manager); + + peerGroup.downloadBlockChain(); + + try { + manager.saveAsText(new File(directory, checkpointsFileName)); + } catch (FileNotFoundException e) { + throw new RuntimeException("Failed to save updated BTC checkpoints", e); + } + } + +} diff --git a/src/data/at/ATData.java b/src/data/at/ATData.java index 388113b3..12afcdef 100644 --- a/src/data/at/ATData.java +++ b/src/data/at/ATData.java @@ -9,6 +9,7 @@ public class ATData { private byte[] creatorPublicKey; private long creation; private int version; + private long assetId; private byte[] codeBytes; private boolean isSleeping; private Integer sleepUntilHeight; @@ -19,12 +20,13 @@ public class ATData { // Constructors - public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, byte[] codeBytes, boolean isSleeping, Integer sleepUntilHeight, - boolean isFinished, boolean hadFatalError, boolean isFrozen, BigDecimal frozenBalance) { + public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, boolean isSleeping, + Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, BigDecimal frozenBalance) { this.ATAddress = ATAddress; this.creatorPublicKey = creatorPublicKey; this.creation = creation; this.version = version; + this.assetId = assetId; this.codeBytes = codeBytes; this.isSleeping = isSleeping; this.sleepUntilHeight = sleepUntilHeight; @@ -34,13 +36,14 @@ public class ATData { this.frozenBalance = frozenBalance; } - public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, byte[] codeBytes, boolean isSleeping, Integer sleepUntilHeight, - boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) { - this(ATAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, (BigDecimal) null); + public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, boolean isSleeping, + Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) { + this(ATAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, + (BigDecimal) null); // Convert Long frozenBalance to BigDecimal if (frozenBalance != null) - this.frozenBalance = BigDecimal.valueOf(frozenBalance).setScale(8).divide(BigDecimal.valueOf(1e8)); + this.frozenBalance = BigDecimal.valueOf(frozenBalance, 8); } // Getters / setters @@ -61,6 +64,10 @@ public class ATData { return this.version; } + public long getAssetId() { + return this.assetId; + } + public byte[] getCodeBytes() { return this.codeBytes; } diff --git a/src/data/block/BlockData.java b/src/data/block/BlockData.java index a7255f43..656b7853 100644 --- a/src/data/block/BlockData.java +++ b/src/data/block/BlockData.java @@ -7,6 +7,8 @@ import java.io.Serializable; public class BlockData implements Serializable { + private static final long serialVersionUID = -7678329659124664620L; + private byte[] signature; private int version; private byte[] reference; @@ -21,8 +23,11 @@ public class BlockData implements Serializable { private int atCount; private BigDecimal atFees; - private BlockData() {} // necessary for JAX-RS serialization - + // necessary for JAX-RS serialization + @SuppressWarnings("unused") + private BlockData() { + } + public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp, BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, int atCount, BigDecimal atFees) { this.version = version; diff --git a/src/data/transaction/DeployATTransactionData.java b/src/data/transaction/DeployATTransactionData.java index 101e2094..b02379e8 100644 --- a/src/data/transaction/DeployATTransactionData.java +++ b/src/data/transaction/DeployATTransactionData.java @@ -13,12 +13,13 @@ public class DeployATTransactionData extends TransactionData { private String tags; private byte[] creationBytes; private BigDecimal amount; + private long assetId; private String ATAddress; // Constructors public DeployATTransactionData(String ATAddress, byte[] creatorPublicKey, String name, String description, String ATType, String tags, byte[] creationBytes, - BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + BigDecimal amount, long assetId, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { super(TransactionType.DEPLOY_AT, fee, creatorPublicKey, timestamp, reference, signature); this.name = name; @@ -26,18 +27,19 @@ public class DeployATTransactionData extends TransactionData { this.ATType = ATType; this.tags = tags; this.amount = amount; + this.assetId = assetId; this.creationBytes = creationBytes; this.ATAddress = ATAddress; } public DeployATTransactionData(byte[] creatorPublicKey, String name, String description, String ATType, String tags, byte[] creationBytes, - BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { - this(null, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, fee, timestamp, reference, signature); + BigDecimal amount, long assetId, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + this(null, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, assetId, fee, timestamp, reference, signature); } public DeployATTransactionData(byte[] creatorPublicKey, String name, String description, String ATType, String tags, byte[] creationBytes, - BigDecimal amount, BigDecimal fee, long timestamp, byte[] reference) { - this(null, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, fee, timestamp, reference, null); + BigDecimal amount, long assetId, BigDecimal fee, long timestamp, byte[] reference) { + this(null, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, assetId, fee, timestamp, reference, null); } // Getters/Setters @@ -66,6 +68,10 @@ public class DeployATTransactionData extends TransactionData { return this.amount; } + public long getAssetId() { + return this.assetId; + } + public String getATAddress() { return this.ATAddress; } diff --git a/src/data/transaction/TransactionData.java b/src/data/transaction/TransactionData.java index b15b1138..1fc544c8 100644 --- a/src/data/transaction/TransactionData.java +++ b/src/data/transaction/TransactionData.java @@ -4,7 +4,6 @@ import java.math.BigDecimal; import java.math.BigInteger; import java.util.Arrays; -import data.account.AccountData; import qora.transaction.Transaction.TransactionType; public abstract class TransactionData { diff --git a/src/qora/at/AT.java b/src/qora/at/AT.java index 10d98a65..232acaec 100644 --- a/src/qora/at/AT.java +++ b/src/qora/at/AT.java @@ -9,6 +9,7 @@ import org.ciyam.at.MachineState; import data.at.ATData; import data.at.ATStateData; import data.transaction.DeployATTransactionData; +import qora.assets.Asset; import qora.crypto.Crypto; import qora.transaction.ATTransaction; import repository.ATRepository; @@ -44,14 +45,15 @@ public class AT { long creation = deployATTransactionData.getTimestamp(); byte[] creationBytes = deployATTransactionData.getCreationBytes(); + long assetId = deployATTransactionData.getAssetId(); short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian if (version >= 2) { MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes()); - this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, machineState.getCodeBytes(), machineState.getIsSleeping(), - machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(), machineState.getIsFrozen(), - machineState.getFrozenBalance()); + this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(), + machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(), + machineState.getIsFrozen(), machineState.getFrozenBalance()); byte[] stateData = machineState.toBytes(); byte[] stateHash = Crypto.digest(stateData); @@ -95,8 +97,8 @@ public class AT { boolean isFrozen = false; Long frozenBalance = null; - this.atData = new ATData(atAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, - frozenBalance); + this.atData = new ATData(atAddress, creatorPublicKey, creation, version, Asset.QORA, codeBytes, isSleeping, sleepUntilHeight, isFinished, + hadFatalError, isFrozen, frozenBalance); this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8)); } diff --git a/src/qora/at/BlockchainAPI.java b/src/qora/at/BlockchainAPI.java new file mode 100644 index 00000000..2edfff46 --- /dev/null +++ b/src/qora/at/BlockchainAPI.java @@ -0,0 +1,134 @@ +package qora.at; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toMap; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +import org.ciyam.at.MachineState; +import org.ciyam.at.Timestamp; + +import data.block.BlockData; +import data.transaction.ATTransactionData; +import data.transaction.PaymentTransactionData; +import data.transaction.TransactionData; +import qora.account.Account; +import qora.block.Block; +import qora.transaction.Transaction; +import repository.BlockRepository; +import repository.DataException; + +public enum BlockchainAPI { + + QORA(0) { + @Override + public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) { + int height = timestamp.blockHeight; + int sequence = timestamp.transactionSequence + 1; + + QoraATAPI api = (QoraATAPI) state.getAPI(); + Account recipientAccount = new Account(api.repository, recipient); + BlockRepository blockRepository = api.repository.getBlockRepository(); + + try { + while (height <= blockRepository.getBlockchainHeight()) { + BlockData blockData = blockRepository.fromHeight(height); + + if (blockData == null) + throw new DataException("Unable to fetch block " + height + " from repository?"); + + Block block = new Block(api.repository, blockData); + + List transactions = block.getTransactions(); + + // No more transactions in this block? Try next block + if (sequence >= transactions.size()) { + ++height; + sequence = 0; + continue; + } + + Transaction transaction = transactions.get(sequence); + + // Transaction needs to be sent to specified recipient + if (transaction.getRecipientAccounts().contains(recipientAccount)) { + // Found a transaction + + api.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[] hash = QoraATAPI.sha192(transaction.getTransactionData().getSignature()); + + api.setA2(state, QoraATAPI.fromBytes(hash, 0)); + api.setA3(state, QoraATAPI.fromBytes(hash, 8)); + api.setA4(state, QoraATAPI.fromBytes(hash, 16)); + return; + } + + // Transaction wasn't for us - keep going + ++sequence; + } + + // No more transactions - zero A and exit + api.zeroA(state); + } catch (DataException e) { + throw new RuntimeException("AT API unable to fetch next transaction?", e); + } + } + + @Override + public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) { + QoraATAPI api = (QoraATAPI) state.getAPI(); + TransactionData transactionData = api.fetchTransaction(state); + + switch (transactionData.getType()) { + case PAYMENT: + return ((PaymentTransactionData) transactionData).getAmount().unscaledValue().longValue(); + + case AT: + BigDecimal amount = ((ATTransactionData) transactionData).getAmount(); + + if (amount != null) + return amount.unscaledValue().longValue(); + else + return 0xffffffffffffffffL; + + default: + return 0xffffffffffffffffL; + } + } + }, + BTC(1) { + @Override + public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) { + // TODO + } + + @Override + public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) { + // TODO + return 0; + } + }; + + public final int value; + + private final static Map map = stream(BlockchainAPI.values()).collect(toMap(type -> type.value, type -> type)); + + BlockchainAPI(int value) { + this.value = value; + } + + public static BlockchainAPI valueOf(int value) { + return map.get(value); + } + + // Blockchain-specific API methods + + public abstract void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state); + + public abstract long getAmountFromTransactionInA(Timestamp timestamp, MachineState state); + +} diff --git a/src/qora/at/QoraATAPI.java b/src/qora/at/QoraATAPI.java index 9fbe7335..897c626f 100644 --- a/src/qora/at/QoraATAPI.java +++ b/src/qora/at/QoraATAPI.java @@ -1,8 +1,6 @@ package qora.at; import java.math.BigDecimal; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -22,16 +20,12 @@ import data.at.ATData; import data.block.BlockData; import data.transaction.ATTransactionData; import data.transaction.MessageTransactionData; -import data.transaction.PaymentTransactionData; import data.transaction.TransactionData; import qora.account.Account; import qora.account.PublicKeyAccount; import qora.assets.Asset; -import qora.block.Block; import qora.crypto.Crypto; import qora.transaction.ATTransaction; -import qora.transaction.Transaction; -import repository.BlockRepository; import repository.DataException; import repository.Repository; @@ -116,10 +110,7 @@ public class QoraATAPI extends API { // Block's signature is 128 bytes so we need to reduce this to 4 longs (32 bytes) byte[] blockHash = Crypto.digest(blockData.getSignature()); - this.setA1(state, fromBytes(blockHash, 0)); - this.setA2(state, fromBytes(blockHash, 8)); - this.setA3(state, fromBytes(blockHash, 16)); - this.setA4(state, fromBytes(blockHash, 24)); + this.setA(state, blockHash); } catch (DataException e) { throw new RuntimeException("AT API unable to fetch previous block?", e); } @@ -127,57 +118,11 @@ public class QoraATAPI extends API { @Override public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) { - // "Timestamp" is block height and transaction sequence - int height = timestamp.blockHeight; - int sequence = timestamp.transactionSequence + 1; + // Recipient is this AT + String recipient = this.atData.getATAddress(); - Account atAccount = new Account(this.repository, this.atData.getATAddress()); - BlockRepository blockRepository = this.repository.getBlockRepository(); - - try { - while (height <= blockRepository.getBlockchainHeight()) { - BlockData blockData = blockRepository.fromHeight(height); - - if (blockData == null) - throw new DataException("Unable to fetch block " + height + " from repository?"); - - Block block = new Block(this.repository, blockData); - - List transactions = block.getTransactions(); - - // No more transactions in this block? Try next block - if (sequence >= transactions.size()) { - ++height; - sequence = 0; - continue; - } - - Transaction transaction = transactions.get(sequence); - - // Transaction needs to be sent to this AT - if (transaction.getRecipientAccounts().contains(atAccount)) { - // Found a transaction - - this.setA1(state, new Timestamp(height, sequence).longValue()); - - // Hash transaction's signature into other three A fields for future verification that it's the same transaction - byte[] hash = sha192(transaction.getTransactionData().getSignature()); - - this.setA2(state, fromBytes(hash, 0)); - this.setA3(state, fromBytes(hash, 8)); - this.setA4(state, fromBytes(hash, 16)); - return; - } - - // Transaction wasn't for us - keep going - ++sequence; - } - - // No more transactions - zero A and exit - this.zeroA(state); - } catch (DataException e) { - throw new RuntimeException("AT API unable to fetch next transaction?", e); - } + BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId); + blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state); } @Override @@ -204,23 +149,9 @@ public class QoraATAPI extends API { @Override public long getAmountFromTransactionInA(MachineState state) { - TransactionData transactionData = this.fetchTransaction(state); - - switch (transactionData.getType()) { - case PAYMENT: - return ((PaymentTransactionData) transactionData).getAmount().unscaledValue().longValue(); - - case AT: - BigDecimal amount = ((ATTransactionData) transactionData).getAmount(); - - if (amount != null) - return amount.unscaledValue().longValue(); - else - return 0xffffffffffffffffL; - - default: - return 0xffffffffffffffffL; - } + Timestamp timestamp = new Timestamp(state.getA1()); + BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId); + return blockchainAPI.getAmountFromTransactionInA(timestamp, state); } @Override @@ -295,13 +226,7 @@ public class QoraATAPI extends API { byte[] paddedMessageData = Bytes.ensureCapacity(messageData, 4 * 8, 0); // Endian must be correct here so that (for example) a SHA256 message can be compared to one generated locally - ByteBuffer digestByteBuffer = ByteBuffer.wrap(paddedMessageData); - digestByteBuffer.order(ByteOrder.LITTLE_ENDIAN); - - this.setB1(state, digestByteBuffer.getLong()); - this.setB2(state, digestByteBuffer.getLong()); - this.setB3(state, digestByteBuffer.getLong()); - this.setB4(state, digestByteBuffer.getLong()); + this.setB(state, paddedMessageData); } @Override @@ -311,14 +236,7 @@ public class QoraATAPI extends API { // We actually use public key as it has more potential utility (e.g. message verification) than an address byte[] bytes = transactionData.getCreatorPublicKey(); - // Enforce endian - ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); - byteBuffer.order(ByteOrder.LITTLE_ENDIAN); - - this.setB1(state, byteBuffer.getLong()); - this.setB2(state, byteBuffer.getLong()); - this.setB3(state, byteBuffer.getLong()); - this.setB4(state, byteBuffer.getLong()); + this.setB(state, bytes); } @Override @@ -326,19 +244,12 @@ public class QoraATAPI extends API { // We actually use public key as it has more potential utility (e.g. message verification) than an address byte[] bytes = atData.getCreatorPublicKey(); - // Enforce endian - ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); - byteBuffer.order(ByteOrder.LITTLE_ENDIAN); - - this.setB1(state, byteBuffer.getLong()); - this.setB2(state, byteBuffer.getLong()); - this.setB3(state, byteBuffer.getLong()); - this.setB4(state, byteBuffer.getLong()); + this.setB(state, bytes); } @Override public long getCurrentBalance(MachineState state) { - Account atAccount = new Account(this.repository, this.atData.getATAddress()); + Account atAccount = this.getATAccount(); try { return atAccount.getConfirmedBalance(Asset.QORA).unscaledValue().longValue(); @@ -348,15 +259,39 @@ public class QoraATAPI extends API { } @Override - public void payAmountToB(long amount, MachineState state) { - // TODO Auto-generated method stub + public void payAmountToB(long unscaledAmount, MachineState state) { + byte[] publicKey = state.getB(); + PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey); + + long timestamp = this.getNextTransactionTimestamp(); + byte[] reference = this.getLastReference(); + BigDecimal amount = BigDecimal.valueOf(unscaledAmount, 8); + + ATTransactionData atTransactionData = new ATTransactionData(this.atData.getATAddress(), recipient.getAddress(), amount, this.atData.getAssetId(), + new byte[0], BigDecimal.ZERO.setScale(8), timestamp, reference); + ATTransaction atTransaction = new ATTransaction(this.repository, atTransactionData); + + // Add to our transactions + this.transactions.add(atTransaction); } @Override public void messageAToB(MachineState state) { - // TODO Auto-generated method stub + byte[] message = state.getA(); + byte[] publicKey = state.getB(); + PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey); + + long timestamp = this.getNextTransactionTimestamp(); + byte[] reference = this.getLastReference(); + + ATTransactionData atTransactionData = new ATTransactionData(this.atData.getATAddress(), recipient.getAddress(), BigDecimal.ZERO, + this.atData.getAssetId(), message, BigDecimal.ZERO.setScale(8), timestamp, reference); + ATTransaction atTransaction = new ATTransaction(this.repository, atTransactionData); + + // Add to our transactions + this.transactions.add(atTransaction); } @Override @@ -377,8 +312,8 @@ public class QoraATAPI extends API { byte[] reference = this.getLastReference(); BigDecimal amount = BigDecimal.valueOf(finalBalance, 8); - ATTransactionData atTransactionData = new ATTransactionData(this.atData.getATAddress(), creator.getAddress(), amount, Asset.QORA, null, - BigDecimal.ZERO.setScale(8), timestamp, reference); + ATTransactionData atTransactionData = new ATTransactionData(this.atData.getATAddress(), creator.getAddress(), amount, this.atData.getAssetId(), + new byte[0], BigDecimal.ZERO.setScale(8), timestamp, reference); ATTransaction atTransaction = new ATTransaction(this.repository, atTransactionData); // Add to our transactions @@ -391,27 +326,33 @@ public class QoraATAPI extends API { } @Override - public void platformSpecificPreExecuteCheck(short functionCodeValue, int paramCount, boolean returnValueExpected) throws IllegalFunctionCodeException { - // Currently not in use - throw new IllegalFunctionCodeException("AT API platform-specific functions currently not in use"); + public void platformSpecificPreExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode) + throws IllegalFunctionCodeException { + QoraFunctionCode qoraFunctionCode = QoraFunctionCode.valueOf(rawFunctionCode); + + if (qoraFunctionCode == null) + throw new IllegalFunctionCodeException("Unknown Qora function code 0x" + String.format("%04x", rawFunctionCode) + " encountered"); + + qoraFunctionCode.preExecuteCheck(2, true, state, rawFunctionCode); } @Override - public void platformSpecificPostCheckExecute(short functionCodeValue, FunctionData functionData, MachineState state) throws ExecutionException { - // Currently not in use - throw new ExecutionException("AT API platform-specific functions currently not in use"); + public void platformSpecificPostCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + QoraFunctionCode qoraFunctionCode = QoraFunctionCode.valueOf(rawFunctionCode); + + qoraFunctionCode.execute(functionData, state, rawFunctionCode); } // Utility methods /** Convert part of little-endian byte[] to long */ - private static long fromBytes(byte[] bytes, int start) { + /* package */ static long fromBytes(byte[] bytes, int start) { return (bytes[start] & 0xffL) | (bytes[start + 1] & 0xffL) << 8 | (bytes[start + 2] & 0xffL) << 16 | (bytes[start + 3] & 0xffL) << 24 | (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 */ - private static byte[] sha192(byte[] input) { + public static byte[] sha192(byte[] input) { try { // SHA2-192 MessageDigest sha192 = MessageDigest.getInstance("SHA-192"); @@ -431,7 +372,7 @@ public class QoraATAPI extends API { } /** Returns transaction data from repository using block height & sequence from A1, checking the transaction signatures match too */ - private TransactionData fetchTransaction(MachineState state) { + /* package */ TransactionData fetchTransaction(MachineState state) { Timestamp timestamp = new Timestamp(state.getA1()); try { @@ -450,6 +391,11 @@ public class QoraATAPI extends API { } } + /** Returns AT's account */ + /* package */ Account getATAccount() { + return new Account(this.repository, this.atData.getATAddress()); + } + /** Returns AT's creator's account */ private PublicKeyAccount getCreator() { return new PublicKeyAccount(this.repository, this.atData.getCreatorPublicKey()); @@ -478,7 +424,7 @@ public class QoraATAPI extends API { return this.transactions.get(this.transactions.size() - 1).getTransactionData().getSignature(); // No transactions yet, so look up AT's account's last reference from repository - Account atAccount = new Account(this.repository, this.atData.getATAddress()); + Account atAccount = this.getATAccount(); try { return atAccount.getLastReference(); diff --git a/src/qora/at/QoraFunctionCode.java b/src/qora/at/QoraFunctionCode.java new file mode 100644 index 00000000..6fadaf99 --- /dev/null +++ b/src/qora/at/QoraFunctionCode.java @@ -0,0 +1,107 @@ +package qora.at; + +import java.io.UnsupportedEncodingException; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +import org.ciyam.at.ExecutionException; +import org.ciyam.at.FunctionData; +import org.ciyam.at.IllegalFunctionCodeException; +import org.ciyam.at.MachineState; +import org.ciyam.at.Timestamp; + +/** + * Qora-specific CIYAM-AT Functions. + *

+ * Function codes need to be between 0x0500 and 0x06ff. + * + */ +public enum QoraFunctionCode { + /** + * 0x0500
+ * Returns current BTC block's "timestamp" + */ + GET_BTC_BLOCK_TIMESTAMP(0x0500, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = Timestamp.toLong(state.getAPI().getCurrentBlockHeight(), BlockchainAPI.BTC.value, 0); + } + }, + /** + * 0x0501
+ * Put transaction from specific recipient after timestamp in A, or zero if none
+ */ + PUT_TX_FROM_B_RECIPIENT_AFTER_TIMESTAMP_IN_A(0x0501, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + Timestamp timestamp = new Timestamp(functionData.value2); + + try { + String recipient = new String(state.getB(), "UTF-8"); + + BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId); + blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state); + } catch (UnsupportedEncodingException e) { + throw new ExecutionException("Couldn't parse recipient from B", e); + } + } + }; + + public final short value; + public final int paramCount; + public final boolean returnsValue; + + private final static Map map = Arrays.stream(QoraFunctionCode.values()) + .collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode)); + + private QoraFunctionCode(int value, int paramCount, boolean returnsValue) { + this.value = (short) value; + this.paramCount = paramCount; + this.returnsValue = returnsValue; + } + + public static QoraFunctionCode valueOf(int value) { + return map.get((short) value); + } + + public void preExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode) throws IllegalFunctionCodeException { + if (paramCount != this.paramCount) + throw new IllegalFunctionCodeException( + "Passed paramCount (" + paramCount + ") does not match function's required paramCount (" + this.paramCount + ")"); + + if (returnValueExpected != this.returnsValue) + throw new IllegalFunctionCodeException( + "Passed returnValueExpected (" + returnValueExpected + ") does not match function's return signature (" + this.returnsValue + ")"); + } + + /** + * Execute Function + *

+ * Can modify various fields of state, including programCounter. + *

+ * Throws a subclass of ExecutionException on error, e.g. InvalidAddressException. + * + * @param functionData + * @param state + * @throws ExecutionException + */ + 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); + + if (functionData.paramCount >= 1 && functionData.value1 == null) + throw new IllegalFunctionCodeException("Passed value1 is null but function has paramCount of (" + this.paramCount + ")"); + + 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() + "\""); + + postCheckExecute(functionData, state, rawFunctionCode); + } + + /** Actually execute function */ + abstract protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException; + +} diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index 3ca9e702..917e52dc 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -258,7 +258,7 @@ public class Block { return 1; else if (this.blockData.getTimestamp() < BlockChain.getPowFixReleaseTimestamp()) return 2; - else if (this.blockData.getTimestamp() < BlockChain.getDeployATV2Timestamp()) + else if (this.blockData.getTimestamp() < BlockChain.getQoraV2Timestamp()) return 3; else return 4; diff --git a/src/qora/block/BlockChain.java b/src/qora/block/BlockChain.java index a35ca067..0029ad92 100644 --- a/src/qora/block/BlockChain.java +++ b/src/qora/block/BlockChain.java @@ -39,11 +39,7 @@ public class BlockChain { private static final long VOTING_RELEASE_TIMESTAMP = 1403715600000L; // 2014-06-25T17:00:00+00:00 private static final long ARBITRARY_RELEASE_TIMESTAMP = 1405702800000L; // 2014-07-18T17:00:00+00:00 - private static final long CREATE_POLL_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 CREATE POLL transactions - private static final long ISSUE_ASSET_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 ISSUE ASSET transactions - private static final long CREATE_ORDER_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 CREATE ORDER transactions - private static final long ARBITRARY_TRANSACTION_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 ARBITRARY transactions - private static final long DEPLOY_AT_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 DEPLOY AT transactions + private static final long QORA_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 blocks and transactions /** * Some sort start-up/initialization/checking method. @@ -146,39 +142,11 @@ public class BlockChain { return ARBITRARY_RELEASE_TIMESTAMP; } - public static long getCreatePollV2Timestamp() { + public static long getQoraV2Timestamp() { if (Settings.getInstance().isTestNet()) return 0; - return CREATE_POLL_V2_TIMESTAMP; - } - - public static long getIssueAssetV2Timestamp() { - if (Settings.getInstance().isTestNet()) - return 0; - - return ISSUE_ASSET_V2_TIMESTAMP; - } - - public static long getCreateOrderV2Timestamp() { - if (Settings.getInstance().isTestNet()) - return 0; - - return CREATE_ORDER_V2_TIMESTAMP; - } - - public static long getArbitraryTransactionV2Timestamp() { - if (Settings.getInstance().isTestNet()) - return 0; - - return ARBITRARY_TRANSACTION_V2_TIMESTAMP; - } - - public static long getDeployATV2Timestamp() { - if (Settings.getInstance().isTestNet()) - return 0; - - return DEPLOY_AT_V2_TIMESTAMP; + return QORA_V2_TIMESTAMP; } } diff --git a/src/qora/payment/Payment.java b/src/qora/payment/Payment.java index 66e2175d..f3ee0c3e 100644 --- a/src/qora/payment/Payment.java +++ b/src/qora/payment/Payment.java @@ -10,6 +10,7 @@ import java.util.Map.Entry; import data.PaymentData; import data.assets.AssetData; +import data.at.ATData; import qora.account.Account; import qora.account.PublicKeyAccount; import qora.assets.Asset; @@ -59,11 +60,20 @@ public class Payment { if (!Crypto.isValidAddress(paymentData.getRecipient())) return ValidationResult.INVALID_ADDRESS; + // Do not allow payments to finished/dead ATs + ATData atData = this.repository.getATRepository().fromATAddress(paymentData.getRecipient()); + if (atData != null && atData.getIsFinished()) + return ValidationResult.AT_IS_FINISHED; + AssetData assetData = assetRepository.fromAssetId(paymentData.getAssetId()); // Check asset even exists if (assetData == null) return ValidationResult.ASSET_DOES_NOT_EXIST; + // If we're sending to an AT then assetId must match AT's assetId + if (atData != null && atData.getAssetId() != paymentData.getAssetId()) + return ValidationResult.ASSET_DOES_NOT_MATCH_AT; + // Check asset amount is integer if asset is not divisible if (!assetData.getIsDivisible() && paymentData.getAmount().stripTrailingZeros().scale() > 0) return ValidationResult.INVALID_AMOUNT; diff --git a/src/qora/transaction/ATTransaction.java b/src/qora/transaction/ATTransaction.java index 2a12e2fd..c750f9e0 100644 --- a/src/qora/transaction/ATTransaction.java +++ b/src/qora/transaction/ATTransaction.java @@ -79,7 +79,8 @@ public class ATTransaction extends Transaction { amount = amount.subtract(this.atTransactionData.getAmount()); } - if (address.equals(this.atTransactionData.getRecipient()) && this.atTransactionData.getAmount() != null) + if (address.equals(this.atTransactionData.getRecipient()) && this.atTransactionData.getAmount() != null + && this.atTransactionData.getAssetId() == Asset.QORA) amount = amount.add(this.atTransactionData.getAmount()); return amount; @@ -118,7 +119,7 @@ public class ATTransaction extends Transaction { return ValidationResult.INVALID_AT_TRANSACTION; // If we have no payment then we're done - if (amount == null) + if (amountIsZero) return ValidationResult.OK; // Check amount is zero or positive diff --git a/src/qora/transaction/DeployATTransaction.java b/src/qora/transaction/DeployATTransaction.java index 042f3c24..0446d90c 100644 --- a/src/qora/transaction/DeployATTransaction.java +++ b/src/qora/transaction/DeployATTransaction.java @@ -12,6 +12,7 @@ import org.ciyam.at.MachineState; import com.google.common.base.Utf8; +import data.assets.AssetData; import data.transaction.DeployATTransactionData; import data.transaction.TransactionData; import qora.account.Account; @@ -157,6 +158,16 @@ public class DeployATTransaction extends Transaction { if (deployATTransactionData.getAmount().compareTo(BigDecimal.ZERO) <= 0) return ValidationResult.NEGATIVE_AMOUNT; + long assetId = deployATTransactionData.getAssetId(); + AssetData assetData = this.repository.getAssetRepository().fromAssetId(assetId); + // Check asset even exists + if (assetData == null) + return ValidationResult.ASSET_DOES_NOT_EXIST; + + // Check asset amount is integer if asset is not divisible + if (!assetData.getIsDivisible() && deployATTransactionData.getAmount().stripTrailingZeros().scale() > 0) + return ValidationResult.INVALID_AMOUNT; + // Check fee is positive if (deployATTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) return ValidationResult.NEGATIVE_FEE; @@ -168,9 +179,19 @@ public class DeployATTransaction extends Transaction { return ValidationResult.INVALID_REFERENCE; // Check creator has enough funds - BigDecimal minimumBalance = deployATTransactionData.getFee().add(deployATTransactionData.getAmount()); - if (creator.getConfirmedBalance(Asset.QORA).compareTo(minimumBalance) < 0) - return ValidationResult.NO_BALANCE; + if (assetId == Asset.QORA) { + // Simple case: amount and fee both in Qora + BigDecimal minimumBalance = deployATTransactionData.getFee().add(deployATTransactionData.getAmount()); + + if (creator.getConfirmedBalance(Asset.QORA).compareTo(minimumBalance) < 0) + return ValidationResult.NO_BALANCE; + } else { + if (creator.getConfirmedBalance(Asset.QORA).compareTo(deployATTransactionData.getFee()) < 0) + return ValidationResult.NO_BALANCE; + + if (creator.getConfirmedBalance(assetId).compareTo(deployATTransactionData.getAmount()) < 0) + return ValidationResult.NO_BALANCE; + } // Check creation bytes are valid (for v2+) if (this.getVersion() >= 2) { @@ -199,9 +220,11 @@ public class DeployATTransaction extends Transaction { // Save this transaction itself this.repository.getTransactionRepository().save(this.transactionData); + long assetId = deployATTransactionData.getAssetId(); + // Update creator's balance Account creator = getCreator(); - creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(deployATTransactionData.getAmount())); + creator.setConfirmedBalance(assetId, creator.getConfirmedBalance(assetId).subtract(deployATTransactionData.getAmount())); creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(deployATTransactionData.getFee())); // Update creator's reference @@ -212,7 +235,7 @@ public class DeployATTransaction extends Transaction { atAccount.setLastReference(deployATTransactionData.getSignature()); // Update AT's balance - atAccount.setConfirmedBalance(Asset.QORA, deployATTransactionData.getAmount()); + atAccount.setConfirmedBalance(assetId, deployATTransactionData.getAmount()); } @Override @@ -224,15 +247,17 @@ public class DeployATTransaction extends Transaction { // Delete this transaction itself this.repository.getTransactionRepository().delete(deployATTransactionData); + long assetId = deployATTransactionData.getAssetId(); + // Update creator's balance Account creator = getCreator(); - creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(deployATTransactionData.getAmount())); + creator.setConfirmedBalance(assetId, creator.getConfirmedBalance(assetId).add(deployATTransactionData.getAmount())); creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(deployATTransactionData.getFee())); // Update creator's reference creator.setLastReference(deployATTransactionData.getReference()); - // Delete AT's account + // Delete AT's account (and hence its balance) this.repository.getAccountRepository().delete(this.deployATTransactionData.getATAddress()); } diff --git a/src/qora/transaction/IssueAssetTransaction.java b/src/qora/transaction/IssueAssetTransaction.java index e1b38e7d..c55ae86b 100644 --- a/src/qora/transaction/IssueAssetTransaction.java +++ b/src/qora/transaction/IssueAssetTransaction.java @@ -120,7 +120,7 @@ public class IssueAssetTransaction extends Transaction { return ValidationResult.NO_BALANCE; // Check the asset name isn't already taken. This check is not present in gen1. - if (issueAssetTransactionData.getTimestamp() >= BlockChain.getIssueAssetV2Timestamp()) + if (issueAssetTransactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp()) if (this.repository.getAssetRepository().assetExists(issueAssetTransactionData.getAssetName())) return ValidationResult.ASSET_ALREADY_EXISTS; diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index d93ad5c4..9eaaeead 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -101,6 +101,8 @@ public abstract class Transaction { INVALID_TAGS_LENGTH(37), INVALID_AT_TYPE_LENGTH(38), INVALID_AT_TRANSACTION(39), + AT_IS_FINISHED(40), + ASSET_DOES_NOT_MATCH_AT(41), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000); @@ -269,8 +271,10 @@ public abstract class Transaction { public static int getVersionByTimestamp(long timestamp) { if (timestamp < BlockChain.getPowFixReleaseTimestamp()) { return 1; - } else { + } else if (timestamp < BlockChain.getQoraV2Timestamp()) { return 3; + } else { + return 4; } } diff --git a/src/repository/TransactionRepository.java b/src/repository/TransactionRepository.java index 7afce46c..dda6c698 100644 --- a/src/repository/TransactionRepository.java +++ b/src/repository/TransactionRepository.java @@ -14,6 +14,7 @@ public interface TransactionRepository { public TransactionData fromHeightAndSequence(int height, int sequence) throws DataException; + /** Returns block height containing transaction or 0 if not in a block or transaction doesn't exist */ public int getHeightFromSignature(byte[] signature) throws DataException; @Deprecated diff --git a/src/repository/hsqldb/HSQLDBATRepository.java b/src/repository/hsqldb/HSQLDBATRepository.java index fb7d7a50..4e980f6a 100644 --- a/src/repository/hsqldb/HSQLDBATRepository.java +++ b/src/repository/hsqldb/HSQLDBATRepository.java @@ -26,7 +26,7 @@ public class HSQLDBATRepository implements ATRepository { @Override public ATData fromATAddress(String atAddress) throws DataException { try (ResultSet resultSet = this.repository.checkedExecute( - "SELECT creator, creation, version, code_bytes, is_sleeping, sleep_until_height, is_finished, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE AT_address = ?", + "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 = ?", atAddress)) { if (resultSet == null) return null; @@ -34,23 +34,24 @@ public class HSQLDBATRepository implements ATRepository { byte[] creatorPublicKey = resultSet.getBytes(1); long creation = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); int version = resultSet.getInt(3); - byte[] codeBytes = resultSet.getBytes(4); // Actually BLOB - boolean isSleeping = resultSet.getBoolean(5); + long assetId = resultSet.getLong(4); + byte[] codeBytes = resultSet.getBytes(5); // XXX: Actually BLOB + boolean isSleeping = resultSet.getBoolean(6); - Integer sleepUntilHeight = resultSet.getInt(6); + Integer sleepUntilHeight = resultSet.getInt(7); if (resultSet.wasNull()) sleepUntilHeight = null; - boolean isFinished = resultSet.getBoolean(7); - boolean hadFatalError = resultSet.getBoolean(8); - boolean isFrozen = resultSet.getBoolean(9); + boolean isFinished = resultSet.getBoolean(8); + boolean hadFatalError = resultSet.getBoolean(9); + boolean isFrozen = resultSet.getBoolean(10); - BigDecimal frozenBalance = resultSet.getBigDecimal(10); + BigDecimal frozenBalance = resultSet.getBigDecimal(11); if (resultSet.wasNull()) frozenBalance = null; - return new ATData(atAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, - frozenBalance); + return new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, + isFrozen, frozenBalance); } catch (SQLException e) { throw new DataException("Unable to fetch AT from repository", e); } @@ -61,7 +62,7 @@ public class HSQLDBATRepository implements ATRepository { List executableATs = new ArrayList(); try (ResultSet resultSet = this.repository.checkedExecute( - "SELECT AT_address, creator, creation, version, code_bytes, is_sleeping, sleep_until_height, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE is_finished = false ORDER BY creation ASC")) { + "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")) { if (resultSet == null) return executableATs; @@ -72,22 +73,23 @@ public class HSQLDBATRepository implements ATRepository { byte[] creatorPublicKey = resultSet.getBytes(2); long creation = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); int version = resultSet.getInt(4); - byte[] codeBytes = resultSet.getBytes(5); // Actually BLOB - boolean isSleeping = resultSet.getBoolean(6); + long assetId = resultSet.getLong(5); + byte[] codeBytes = resultSet.getBytes(6); // XXX: Actually BLOB + boolean isSleeping = resultSet.getBoolean(7); - Integer sleepUntilHeight = resultSet.getInt(7); + Integer sleepUntilHeight = resultSet.getInt(8); if (resultSet.wasNull()) sleepUntilHeight = null; - boolean hadFatalError = resultSet.getBoolean(8); - boolean isFrozen = resultSet.getBoolean(9); + boolean hadFatalError = resultSet.getBoolean(9); + boolean isFrozen = resultSet.getBoolean(10); - BigDecimal frozenBalance = resultSet.getBigDecimal(10); + BigDecimal frozenBalance = resultSet.getBigDecimal(11); if (resultSet.wasNull()) frozenBalance = null; - ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, - frozenBalance); + ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished, + hadFatalError, isFrozen, frozenBalance); executableATs.add(atData); } while (resultSet.next()); @@ -117,9 +119,10 @@ public class HSQLDBATRepository implements ATRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("ATs"); saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).bind("creation", new Timestamp(atData.getCreation())) - .bind("version", atData.getVersion()).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()).bind("frozen_balance", atData.getFrozenBalance()); + .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()) + .bind("frozen_balance", atData.getFrozenBalance()); try { saveHelper.execute(this.repository); diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java index 40afdce9..0aa8c468 100644 --- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -222,7 +222,7 @@ public class HSQLDBDatabaseUpdates { break; case 12: - // Arbitrary/Multi-payment Transaction Payments + // Arbitrary/Multi-payment/Message/Payment Transaction Payments stmt.execute("CREATE TABLE SharedTransactionPayments (signature Signature, recipient QoraAddress NOT NULL, " + "amount QoraAmount NOT NULL, asset_id AssetID NOT NULL, " + "PRIMARY KEY (signature, recipient, asset_id), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); @@ -277,7 +277,7 @@ public class HSQLDBDatabaseUpdates { // Deploy CIYAM AT Transactions stmt.execute("CREATE TABLE DeployATTransactions (signature Signature, creator QoraPublicKey NOT NULL, AT_name ATName NOT NULL, " + "description VARCHAR(2000) NOT NULL, AT_type ATType NOT NULL, AT_tags VARCHAR(200) NOT NULL, " - + "creation_bytes VARBINARY(100000) NOT NULL, amount QoraAmount NOT NULL, AT_address QoraAddress, " + + "creation_bytes VARBINARY(100000) NOT NULL, amount QoraAmount NOT NULL, asset_id AssetID NOT NULL, AT_address QoraAddress, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); // For looking up the Deploy AT Transaction based on deployed AT address stmt.execute("CREATE INDEX DeployATAddressIndex on DeployATTransactions (AT_address)"); @@ -360,7 +360,7 @@ public class HSQLDBDatabaseUpdates { // CIYAM Automated Transactions stmt.execute( "CREATE TABLE ATs (AT_address QoraAddress, creator QoraPublicKey, creation TIMESTAMP WITH TIME ZONE, version INTEGER NOT NULL, " - + "code_bytes ATCode NOT NULL, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, " + + "asset_id AssetID NOT NULL, code_bytes ATCode NOT NULL, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, " + "is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, is_frozen BOOLEAN NOT NULL, frozen_balance QoraAmount, " + "PRIMARY key (AT_address))"); // For finding executable ATs, ordered by creation timestamp diff --git a/src/repository/hsqldb/transaction/HSQLDBDeployATTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBDeployATTransactionRepository.java index a4d54d41..2ec30e68 100644 --- a/src/repository/hsqldb/transaction/HSQLDBDeployATTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBDeployATTransactionRepository.java @@ -18,7 +18,8 @@ public class HSQLDBDeployATTransactionRepository extends HSQLDBTransactionReposi TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException { try (ResultSet resultSet = this.repository.checkedExecute( - "SELECT AT_name, description, AT_type, AT_tags, creation_bytes, amount, AT_address FROM DeployATTransactions WHERE signature = ?", signature)) { + "SELECT AT_name, description, AT_type, AT_tags, creation_bytes, amount, asset_id, AT_address FROM DeployATTransactions WHERE signature = ?", + signature)) { if (resultSet == null) return null; @@ -28,14 +29,15 @@ public class HSQLDBDeployATTransactionRepository extends HSQLDBTransactionReposi String tags = resultSet.getString(4); byte[] creationBytes = resultSet.getBytes(5); BigDecimal amount = resultSet.getBigDecimal(6).setScale(8); + long assetId = resultSet.getLong(7); // Special null-checking for AT address - String ATAddress = resultSet.getString(7); + String ATAddress = resultSet.getString(8); if (resultSet.wasNull()) ATAddress = null; - return new DeployATTransactionData(ATAddress, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, fee, timestamp, reference, - signature); + return new DeployATTransactionData(ATAddress, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, assetId, fee, timestamp, + reference, signature); } catch (SQLException e) { throw new DataException("Unable to fetch deploy AT transaction from repository", e); } @@ -51,7 +53,7 @@ public class HSQLDBDeployATTransactionRepository extends HSQLDBTransactionReposi .bind("AT_name", deployATTransactionData.getName()).bind("description", deployATTransactionData.getDescription()) .bind("AT_type", deployATTransactionData.getATType()).bind("AT_tags", deployATTransactionData.getTags()) .bind("creation_bytes", deployATTransactionData.getCreationBytes()).bind("amount", deployATTransactionData.getAmount()) - .bind("AT_address", deployATTransactionData.getATAddress()); + .bind("asset_id", deployATTransactionData.getAssetId()).bind("AT_address", deployATTransactionData.getATAddress()); try { saveHelper.execute(this.repository); diff --git a/src/settings/Settings.java b/src/settings/Settings.java index 090f4a21..4c94358f 100644 --- a/src/settings/Settings.java +++ b/src/settings/Settings.java @@ -5,7 +5,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Set; import org.json.simple.JSONArray; import org.json.simple.JSONObject; diff --git a/src/transform/transaction/ATTransactionTransformer.java b/src/transform/transaction/ATTransactionTransformer.java index ff83272b..26743fe1 100644 --- a/src/transform/transaction/ATTransactionTransformer.java +++ b/src/transform/transaction/ATTransactionTransformer.java @@ -2,6 +2,7 @@ package transform.transaction; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.math.BigDecimal; import java.nio.ByteBuffer; import org.json.simple.JSONObject; @@ -23,7 +24,8 @@ public class ATTransactionTransformer extends TransactionTransformer { private static final int ASSET_ID_LENGTH = LONG_LENGTH; private static final int DATA_SIZE_LENGTH = INT_LENGTH; - private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH + ASSET_ID_LENGTH + DATA_SIZE_LENGTH; + private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH + ASSET_ID_LENGTH + + DATA_SIZE_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { throw new TransformationException("Serialized AT Transactions should not exist!"); @@ -49,7 +51,8 @@ public class ATTransactionTransformer extends TransactionTransformer { Serialization.serializeAddress(bytes, atTransactionData.getRecipient()); - if (atTransactionData.getAssetId() != null) { + // Only emit amount if greater than zero (safer than checking assetId) + if (atTransactionData.getAmount().compareTo(BigDecimal.ZERO) > 0) { Serialization.serializeBigDecimal(bytes, atTransactionData.getAmount()); bytes.write(Longs.toByteArray(atTransactionData.getAssetId())); } diff --git a/src/transform/transaction/ArbitraryTransactionTransformer.java b/src/transform/transaction/ArbitraryTransactionTransformer.java index fac8f34e..d77888be 100644 --- a/src/transform/transaction/ArbitraryTransactionTransformer.java +++ b/src/transform/transaction/ArbitraryTransactionTransformer.java @@ -50,7 +50,6 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer { // V3+ allows payments but always return a list of payments, even if empty List payments = new ArrayList(); - ; if (version != 1) { int paymentsCount = byteBuffer.getInt(); @@ -133,7 +132,7 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer { ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData; byte[] bytes = TransactionTransformer.toBytesForSigningImpl(transactionData); - if (arbitraryTransactionData.getVersion() == 1 || transactionData.getTimestamp() >= BlockChain.getArbitraryTransactionV2Timestamp()) + if (arbitraryTransactionData.getVersion() == 1 || transactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp()) return bytes; // Special v1 version diff --git a/src/transform/transaction/CreateOrderTransactionTransformer.java b/src/transform/transaction/CreateOrderTransactionTransformer.java index d8d37827..ef235e31 100644 --- a/src/transform/transaction/CreateOrderTransactionTransformer.java +++ b/src/transform/transaction/CreateOrderTransactionTransformer.java @@ -90,7 +90,7 @@ public class CreateOrderTransactionTransformer extends TransactionTransformer { * @throws TransformationException */ public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException { - if (transactionData.getTimestamp() >= BlockChain.getCreateOrderV2Timestamp()) + if (transactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp()) return TransactionTransformer.toBytesForSigningImpl(transactionData); // Special v1 version diff --git a/src/transform/transaction/CreatePollTransactionTransformer.java b/src/transform/transaction/CreatePollTransactionTransformer.java index 464827c3..0c637df8 100644 --- a/src/transform/transaction/CreatePollTransactionTransformer.java +++ b/src/transform/transaction/CreatePollTransactionTransformer.java @@ -62,7 +62,7 @@ public class CreatePollTransactionTransformer extends TransactionTransformer { pollOptions.add(new PollOptionData(optionName)); // V1 only: voter count also present - if (timestamp < BlockChain.getCreatePollV2Timestamp()) { + if (timestamp < BlockChain.getQoraV2Timestamp()) { int voterCount = byteBuffer.getInt(); if (voterCount != 0) throw new TransformationException("Unexpected voter count in byte data for CreatePollTransaction"); @@ -88,7 +88,7 @@ public class CreatePollTransactionTransformer extends TransactionTransformer { // option-string-length, option-string dataLength += INT_LENGTH + Utf8.encodedLength(pollOptionData.getOptionName()); - if (transactionData.getTimestamp() < BlockChain.getCreatePollV2Timestamp()) + if (transactionData.getTimestamp() < BlockChain.getQoraV2Timestamp()) // v1 only: voter-count (should always be zero) dataLength += INT_LENGTH; } @@ -120,7 +120,7 @@ public class CreatePollTransactionTransformer extends TransactionTransformer { for (PollOptionData pollOptionData : pollOptions) { Serialization.serializeSizedString(bytes, pollOptionData.getOptionName()); - if (transactionData.getTimestamp() < BlockChain.getCreatePollV2Timestamp()) { + if (transactionData.getTimestamp() < BlockChain.getQoraV2Timestamp()) { // In v1, CreatePollTransaction uses Poll.toBytes which serializes voters too. // Zero voters as this is a new poll. bytes.write(Ints.toByteArray(0)); @@ -149,7 +149,7 @@ public class CreatePollTransactionTransformer extends TransactionTransformer { public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException { byte[] bytes = TransactionTransformer.toBytesForSigningImpl(transactionData); - if (transactionData.getTimestamp() >= BlockChain.getCreatePollV2Timestamp()) + if (transactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp()) return bytes; // Special v1 version diff --git a/src/transform/transaction/DeployATTransactionTransformer.java b/src/transform/transaction/DeployATTransactionTransformer.java index 12d78906..ca0077d4 100644 --- a/src/transform/transaction/DeployATTransactionTransformer.java +++ b/src/transform/transaction/DeployATTransactionTransformer.java @@ -14,6 +14,7 @@ import com.google.common.primitives.Longs; import data.transaction.TransactionData; import qora.account.PublicKeyAccount; +import qora.assets.Asset; import qora.block.BlockChain; import qora.transaction.DeployATTransaction; import data.transaction.DeployATTransactionData; @@ -30,13 +31,17 @@ public class DeployATTransactionTransformer extends TransactionTransformer { private static final int TAGS_SIZE_LENGTH = INT_LENGTH; private static final int CREATION_BYTES_SIZE_LENGTH = INT_LENGTH; private static final int AMOUNT_LENGTH = LONG_LENGTH; + private static final int ASSET_ID_LENGTH = LONG_LENGTH; private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + CREATOR_LENGTH + NAME_SIZE_LENGTH + DESCRIPTION_SIZE_LENGTH + AT_TYPE_SIZE_LENGTH + TAGS_SIZE_LENGTH + CREATION_BYTES_SIZE_LENGTH + AMOUNT_LENGTH; + private static final int V4_TYPELESS_LENGTH = TYPELESS_LENGTH + ASSET_ID_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { long timestamp = byteBuffer.getLong(); + int version = DeployATTransaction.getVersionByTimestamp(timestamp); + byte[] reference = new byte[REFERENCE_LENGTH]; byteBuffer.get(reference); @@ -59,20 +64,34 @@ public class DeployATTransactionTransformer extends TransactionTransformer { BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); + long assetId = Asset.QORA; + if (version >= 4) + assetId = byteBuffer.getLong(); + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); byte[] signature = new byte[SIGNATURE_LENGTH]; byteBuffer.get(signature); - return new DeployATTransactionData(creatorPublicKey, name, description, ATType, tags, creationBytes, amount, fee, timestamp, reference, signature); + return new DeployATTransactionData(creatorPublicKey, name, description, ATType, tags, creationBytes, amount, assetId, fee, timestamp, reference, + signature); } public static int getDataLength(TransactionData transactionData) throws TransformationException { DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData; - int dataLength = TYPE_LENGTH + TYPELESS_LENGTH + Utf8.encodedLength(deployATTransactionData.getName()) - + Utf8.encodedLength(deployATTransactionData.getDescription()) + Utf8.encodedLength(deployATTransactionData.getATType()) - + Utf8.encodedLength(deployATTransactionData.getTags()) + deployATTransactionData.getCreationBytes().length; + int dataLength = TYPE_LENGTH; + + int version = DeployATTransaction.getVersionByTimestamp(transactionData.getTimestamp()); + + if (version >= 4) + dataLength += V4_TYPELESS_LENGTH; + else + dataLength += TYPELESS_LENGTH; + + dataLength += Utf8.encodedLength(deployATTransactionData.getName()) + Utf8.encodedLength(deployATTransactionData.getDescription()) + + Utf8.encodedLength(deployATTransactionData.getATType()) + Utf8.encodedLength(deployATTransactionData.getTags()) + + deployATTransactionData.getCreationBytes().length; return dataLength; } @@ -81,6 +100,8 @@ public class DeployATTransactionTransformer extends TransactionTransformer { try { DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData; + int version = DeployATTransaction.getVersionByTimestamp(transactionData.getTimestamp()); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); bytes.write(Ints.toByteArray(deployATTransactionData.getType().value)); @@ -103,6 +124,9 @@ public class DeployATTransactionTransformer extends TransactionTransformer { Serialization.serializeBigDecimal(bytes, deployATTransactionData.getAmount()); + if (version >= 4) + bytes.write(Longs.toByteArray(deployATTransactionData.getAssetId())); + Serialization.serializeBigDecimal(bytes, deployATTransactionData.getFee()); if (deployATTransactionData.getSignature() != null) @@ -115,20 +139,19 @@ public class DeployATTransactionTransformer extends TransactionTransformer { } /** - * In Qora v1, the bytes used for verification omit AT-type and tags so we need to test for v1-ness and adjust the bytes - * accordingly. + * In Qora v1, the bytes used for verification omit AT-type and tags so we need to test for v1-ness and adjust the bytes accordingly. * * @param transactionData * @return byte[] * @throws TransformationException */ public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException { - if (transactionData.getTimestamp() >= BlockChain.getDeployATV2Timestamp()) + if (transactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp()) return TransactionTransformer.toBytesForSigningImpl(transactionData); // Special v1 version - // Easier to start from scratch + // Easier to start from scratch try { DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData; @@ -179,6 +202,7 @@ public class DeployATTransactionTransformer extends TransactionTransformer { json.put("tags", deployATTransactionData.getTags()); json.put("creationBytes", HashCode.fromBytes(deployATTransactionData.getCreationBytes()).toString()); json.put("amount", deployATTransactionData.getAmount().toPlainString()); + json.put("assetId", deployATTransactionData.getAssetId()); } catch (ClassCastException e) { throw new TransformationException(e); } diff --git a/src/transform/transaction/IssueAssetTransactionTransformer.java b/src/transform/transaction/IssueAssetTransactionTransformer.java index a4ec1b5d..43a53171 100644 --- a/src/transform/transaction/IssueAssetTransactionTransformer.java +++ b/src/transform/transaction/IssueAssetTransactionTransformer.java @@ -55,7 +55,7 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { byte[] assetReference = new byte[ASSET_REFERENCE_LENGTH]; // In v1, IssueAssetTransaction uses Asset.parse which also deserializes reference. - if (timestamp < BlockChain.getIssueAssetV2Timestamp()) + if (timestamp < BlockChain.getQoraV2Timestamp()) byteBuffer.get(assetReference); BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); @@ -73,7 +73,7 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { + Utf8.encodedLength(issueAssetTransactionData.getDescription()); // In v1, IssueAssetTransaction uses Asset.toBytes which also serializes reference. - if (transactionData.getTimestamp() < BlockChain.getIssueAssetV2Timestamp()) + if (transactionData.getTimestamp() < BlockChain.getQoraV2Timestamp()) dataLength += ASSET_REFERENCE_LENGTH; return dataLength; @@ -100,7 +100,7 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { bytes.write((byte) (issueAssetTransactionData.getIsDivisible() ? 1 : 0)); // In v1, IssueAssetTransaction uses Asset.toBytes which also serializes Asset's reference which is the IssueAssetTransaction's signature - if (transactionData.getTimestamp() < BlockChain.getIssueAssetV2Timestamp()) { + if (transactionData.getTimestamp() < BlockChain.getQoraV2Timestamp()) { byte[] assetReference = issueAssetTransactionData.getSignature(); if (assetReference != null) bytes.write(assetReference); @@ -130,7 +130,7 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException { byte[] bytes = TransactionTransformer.toBytesForSigningImpl(transactionData); - if (transactionData.getTimestamp() >= BlockChain.getIssueAssetV2Timestamp()) + if (transactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp()) return bytes; // Special v1 version diff --git a/src/transform/transaction/MultiPaymentTransactionTransformer.java b/src/transform/transaction/MultiPaymentTransactionTransformer.java index 470b1866..b1a54c49 100644 --- a/src/transform/transaction/MultiPaymentTransactionTransformer.java +++ b/src/transform/transaction/MultiPaymentTransactionTransformer.java @@ -99,7 +99,7 @@ public class MultiPaymentTransactionTransformer extends TransactionTransformer { public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException { byte[] bytes = TransactionTransformer.toBytesForSigningImpl(transactionData); - if (transactionData.getTimestamp() >= BlockChain.getIssueAssetV2Timestamp()) + if (transactionData.getTimestamp() >= BlockChain.getQoraV2Timestamp()) return bytes; // Special v1 version diff --git a/tests/test/BTCACCTTests.java b/tests/test/BTCACCTTests.java new file mode 100644 index 00000000..881bcc26 --- /dev/null +++ b/tests/test/BTCACCTTests.java @@ -0,0 +1,332 @@ +package test; + +import java.io.File; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.Security; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.Transaction.SigHash; +import org.bitcoinj.core.TransactionBroadcast; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutPoint; +import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.kits.WalletAppKit; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.params.TestNet3Params; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.script.ScriptChunk; +import org.bitcoinj.script.ScriptOpCodes; +import org.bitcoinj.wallet.WalletTransaction.Pool; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.google.common.hash.HashCode; +import com.google.common.primitives.Bytes; + +/** + * Initiator must be Qora-chain so that initiator can send initial message to BTC P2SH then Qora can scan for P2SH add send corresponding message to Qora AT. + * + * Initiator (wants Qora, has BTC) + * Funds BTC P2SH address + * + * Responder (has Qora, wants BTC) + * Builds Qora ACCT AT and funds it with Qora + * + * Initiator sends recipient+secret+script as input to BTC P2SH address, releasing BTC amount - fees to responder + * + * Qora nodes scan for P2SH output, checks amount and recipient and if ok sends secret to Qora ACCT AT + * (Or it's possible to feed BTC transaction details into Qora AT so it can check them itself?) + * + * Qora ACCT AT sends its Qora to initiator + * + */ + +public class BTCACCTTests { + + private static final long TIMEOUT = 600L; + private static final Coin sendValue = Coin.valueOf(6_000L); + private static final Coin fee = Coin.valueOf(2_000L); + + private static final byte[] senderPrivKeyBytes = HashCode.fromString("027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c").asBytes(); + private static final byte[] recipientPrivKeyBytes = HashCode.fromString("ec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03").asBytes(); + + // The following need to be updated manually + private static final String prevTxHash = "70ee97f20afea916c2e7b47f6abf3c75f97c4c2251b4625419406a2dd47d16b5"; + private static final Coin prevTxBalance = Coin.valueOf(562_000L); // This is NOT the amount but the unspent balance + private static final long prevTxOutputIndex = 1L; + + // For when we want to re-run + private static final byte[] prevSecret = HashCode.fromString("30a13291e350214bea5318f990b77bc11d2cb709f7c39859f248bef396961dcc").asBytes(); + private static final long prevLockTime = 1539347892L; + private static final boolean usePreviousFundingTx = true; + + private static final boolean doRefundNotRedeem = false; + + @BeforeClass + public static void beforeClass() { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + } + + @Test + public void buildBTCACCTTest() throws NoSuchAlgorithmException, InsufficientMoneyException, InterruptedException, ExecutionException, UnknownHostException { + byte[] secret = new byte[32]; + new SecureRandom().nextBytes(secret); + + if (usePreviousFundingTx) + secret = prevSecret; + + System.out.println("Secret: " + HashCode.fromBytes(secret).toString()); + + MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256"); + + byte[] secretHash = sha256Digester.digest(secret); + String secretHashHex = HashCode.fromBytes(secretHash).toString(); + + System.out.println("SHA256(secret): " + secretHashHex); + + NetworkParameters params = TestNet3Params.get(); + // NetworkParameters params = RegTestParams.get(); + System.out.println("Network: " + params.getId()); + + WalletAppKit kit = new WalletAppKit(params, new File("."), "btc-tests"); + + kit.setBlockingStartup(false); + kit.startAsync(); + kit.awaitRunning(); + + long now = System.currentTimeMillis() / 1000L; + long lockTime = now + TIMEOUT; + + if (usePreviousFundingTx) + lockTime = prevLockTime; + + System.out.println("LockTime: " + lockTime); + + ECKey senderKey = ECKey.fromPrivate(senderPrivKeyBytes); + kit.wallet().importKey(senderKey); + ECKey recipientKey = ECKey.fromPrivate(recipientPrivKeyBytes); + kit.wallet().importKey(recipientKey); + + byte[] senderPubKey = senderKey.getPubKey(); + System.out.println("Sender address: " + senderKey.toAddress(params).toBase58()); + System.out.println("Sender pubkey: " + HashCode.fromBytes(senderPubKey).toString()); + + byte[] recipientPubKey = recipientKey.getPubKey(); + System.out.println("Recipient address: " + recipientKey.toAddress(params).toBase58()); + System.out.println("Recipient pubkey: " + HashCode.fromBytes(recipientPubKey).toString()); + + byte[] redeemScriptBytes = buildRedeemScript(secret, senderPubKey, recipientPubKey, lockTime); + System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString()); + + byte[] redeemScriptHash = hash160(redeemScriptBytes); + + Address p2shAddress = Address.fromP2SHHash(params, redeemScriptHash); + System.out.println("P2SH address: " + p2shAddress.toBase58()); + + // Send amount to P2SH address + Transaction fundingTransaction = buildFundingTransaction(params, Sha256Hash.wrap(prevTxHash), prevTxOutputIndex, prevTxBalance, senderKey, + sendValue.add(fee), redeemScriptHash); + + System.out.println("Sending " + sendValue.add(fee).toPlainString() + " to " + p2shAddress.toBase58()); + if (!usePreviousFundingTx) + broadcastWithConfirmation(kit, fundingTransaction); + + if (doRefundNotRedeem) { + // Refund + System.out.println("Refunding " + sendValue.toPlainString() + " back to " + senderKey.toAddress(params)); + + now = System.currentTimeMillis() / 1000L; + long refundLockTime = now - 60 * 30; // 30 minutes in the past, needs to before 'now' and before "median block time" (median of previous 11 block + // timestamps) + if (refundLockTime < lockTime) + throw new RuntimeException("Too soon to refund"); + + TransactionOutPoint fundingOutPoint = new TransactionOutPoint(params, 0, fundingTransaction); + Transaction refundTransaction = buildRefundTransaction(params, fundingOutPoint, senderKey, sendValue, redeemScriptBytes, refundLockTime); + broadcastWithConfirmation(kit, refundTransaction); + } else { + // Redeem + System.out.println("Redeeming " + sendValue.toPlainString() + " to " + recipientKey.toAddress(params)); + + TransactionOutPoint fundingOutPoint = new TransactionOutPoint(params, 0, fundingTransaction); + Transaction redeemTransaction = buildRedeemTransaction(params, fundingOutPoint, recipientKey, sendValue, secret, redeemScriptBytes); + broadcastWithConfirmation(kit, redeemTransaction); + } + + kit.wallet().cleanup(); + + for (Transaction transaction : kit.wallet().getTransactionPool(Pool.PENDING).values()) + System.out.println("Pending tx: " + transaction.getHashAsString()); + } + + private static final byte[] redeemScript1 = HashCode.fromString("76a820").asBytes(); + private static final byte[] redeemScript2 = HashCode.fromString("87637576a914").asBytes(); + private static final byte[] redeemScript3 = HashCode.fromString("88ac6704").asBytes(); + private static final byte[] redeemScript4 = HashCode.fromString("b17576a914").asBytes(); + private static final byte[] redeemScript5 = HashCode.fromString("88ac68").asBytes(); + + private byte[] buildRedeemScript(byte[] secret, byte[] senderPubKey, byte[] recipientPubKey, long lockTime) { + try { + MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256"); + + byte[] secretHash = sha256Digester.digest(secret); + byte[] senderPubKeyHash = hash160(senderPubKey); + byte[] recipientPubKeyHash = hash160(recipientPubKey); + + return Bytes.concat(redeemScript1, secretHash, redeemScript2, recipientPubKeyHash, redeemScript3, toLEByteArray((int) (lockTime & 0xffffffffL)), + redeemScript4, senderPubKeyHash, redeemScript5); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Message digest unsupported", e); + } + } + + private byte[] hash160(byte[] input) { + try { + MessageDigest rmd160Digester = MessageDigest.getInstance("RIPEMD160"); + MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256"); + + return rmd160Digester.digest(sha256Digester.digest(input)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Message digest unsupported", e); + } + } + + private Transaction buildFundingTransaction(NetworkParameters params, Sha256Hash prevTxHash, long outputIndex, Coin balance, ECKey sigKey, Coin value, + byte[] redeemScriptHash) { + Transaction fundingTransaction = new Transaction(params); + + // Outputs (needed before input so inputs can be signed) + // Fixed amount to P2SH + fundingTransaction.addOutput(value, ScriptBuilder.createP2SHOutputScript(redeemScriptHash)); + // Change to sender + fundingTransaction.addOutput(balance.minus(value).minus(fee), ScriptBuilder.createOutputScript(sigKey.toAddress(params))); + + // Input + // We create fake "to address" scriptPubKey for prev tx so our spending input is P2PKH type + Script fakeScriptPubKey = ScriptBuilder.createOutputScript(sigKey.toAddress(params)); + TransactionOutPoint prevOut = new TransactionOutPoint(params, outputIndex, prevTxHash); + fundingTransaction.addSignedInput(prevOut, fakeScriptPubKey, sigKey); + + return fundingTransaction; + } + + private Transaction buildRedeemTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey recipientKey, Coin value, byte[] secret, + byte[] redeemScriptBytes) { + Transaction redeemTransaction = new Transaction(params); + redeemTransaction.setVersion(2); + + // Outputs + redeemTransaction.addOutput(value, ScriptBuilder.createOutputScript(recipientKey.toAddress(params))); + + // Input + byte[] recipientPubKey = recipientKey.getPubKey(); + ScriptBuilder scriptBuilder = new ScriptBuilder(); + scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey)); + scriptBuilder.addChunk(new ScriptChunk(secret.length, secret)); + scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes)); + byte[] scriptPubKey = scriptBuilder.build().getProgram(); + + TransactionInput input = new TransactionInput(params, null, scriptPubKey, fundingOutPoint); + input.setSequenceNumber(0xffffffffL); // Final + redeemTransaction.addInput(input); + + // Generate transaction signature for input + boolean anyoneCanPay = false; + Sha256Hash hash = redeemTransaction.hashForSignature(0, redeemScriptBytes, SigHash.ALL, anyoneCanPay); + System.out.println("redeem transaction's input hash: " + hash.toString()); + + ECKey.ECDSASignature ecSig = recipientKey.sign(hash); + TransactionSignature txSig = new TransactionSignature(ecSig, SigHash.ALL, anyoneCanPay); + byte[] txSigBytes = txSig.encodeToBitcoin(); + System.out.println("redeem transaction's signature: " + HashCode.fromBytes(txSigBytes).toString()); + + // Prepend signature to input + scriptBuilder.addChunk(0, new ScriptChunk(txSigBytes.length, txSigBytes)); + input.setScriptSig(scriptBuilder.build()); + + return redeemTransaction; + } + + private Transaction buildRefundTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey senderKey, Coin value, + byte[] redeemScriptBytes, long lockTime) { + Transaction refundTransaction = new Transaction(params); + refundTransaction.setVersion(2); + + // Outputs + refundTransaction.addOutput(value, ScriptBuilder.createOutputScript(senderKey.toAddress(params))); + + // Input + byte[] recipientPubKey = senderKey.getPubKey(); + ScriptBuilder scriptBuilder = new ScriptBuilder(); + scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey)); + scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes)); + byte[] scriptPubKey = scriptBuilder.build().getProgram(); + + TransactionInput input = new TransactionInput(params, null, scriptPubKey, fundingOutPoint); + input.setSequenceNumber(0); + refundTransaction.addInput(input); + + // Set locktime after input but before input signature is generated + refundTransaction.setLockTime(lockTime); + + // Generate transaction signature for input + boolean anyoneCanPay = false; + Sha256Hash hash = refundTransaction.hashForSignature(0, redeemScriptBytes, SigHash.ALL, anyoneCanPay); + System.out.println("refund transaction's input hash: " + hash.toString()); + + ECKey.ECDSASignature ecSig = senderKey.sign(hash); + TransactionSignature txSig = new TransactionSignature(ecSig, SigHash.ALL, anyoneCanPay); + byte[] txSigBytes = txSig.encodeToBitcoin(); + System.out.println("refund transaction's signature: " + HashCode.fromBytes(txSigBytes).toString()); + + // Prepend signature to input + scriptBuilder.addChunk(0, new ScriptChunk(txSigBytes.length, txSigBytes)); + input.setScriptSig(scriptBuilder.build()); + + return refundTransaction; + } + + private void broadcastWithConfirmation(WalletAppKit kit, Transaction transaction) { + System.out.println("Broadcasting tx: " + transaction.getHashAsString()); + System.out.println("TX hex: " + HashCode.fromBytes(transaction.bitcoinSerialize()).toString()); + + System.out.println("Number of connected peers: " + kit.peerGroup().numConnectedPeers()); + TransactionBroadcast txBroadcast = kit.peerGroup().broadcastTransaction(transaction); + + try { + txBroadcast.future().get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException("Transaction broadcast failed", e); + } + + // wait for confirmation + System.out.println("Waiting for confirmation of tx: " + transaction.getHashAsString()); + + try { + transaction.getConfidence().getDepthFuture(1).get(); + } catch (CancellationException | ExecutionException | InterruptedException e) { + throw new RuntimeException("Transaction confirmation failed", e); + } + + System.out.println("Confirmed tx: " + transaction.getHashAsString()); + } + + /** Convert int to little-endian byte array */ + private byte[] toLEByteArray(int value) { + return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) }; + } + +} diff --git a/tests/test/BTCTests.java b/tests/test/BTCTests.java new file mode 100644 index 00000000..d2ba3030 --- /dev/null +++ b/tests/test/BTCTests.java @@ -0,0 +1,57 @@ +package test; + +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.junit.Test; + +import com.google.common.hash.HashCode; + +import crosschain.BTC; + +public class BTCTests { + + @Test + public void testWatchAddress() throws Exception { + // String testAddress = "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M"; + String testAddress = "1GRENT17xMQe2ukPhwAeZU1TaUUon1Qc65"; + + long testStartTime = 1539000000L; + + BTC btc = BTC.getInstance(); + + btc.watch(testAddress, testStartTime); + + Thread.sleep(5000); + + btc.watch(testAddress, testStartTime); + + btc.shutdown(); + } + + @Test + public void testWatchScript() throws Exception { + long testStartTime = 1539000000L; + + BTC btc = BTC.getInstance(); + + byte[] redeemScriptHash = HashCode.fromString("3dbcc35e69ebc449f616fa3eb3723dfad9cbb5b3").asBytes(); + Script redeemScript = ScriptBuilder.createP2SHOutputScript(redeemScriptHash); + redeemScript.setCreationTimeSeconds(testStartTime); + + // btc.watch(redeemScript); + + Thread.sleep(5000); + + // btc.watch(redeemScript); + + btc.shutdown(); + } + + @Test + public void updateCheckpoints() throws Exception { + BTC btc = BTC.getInstance(); + + btc.updateCheckpoints(); + } + +}