diff --git a/README.md b/README.md index 7837af25..27c53351 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,13 @@ To use: - Use maven to fetch dependencies. - Build project. +- Build v1feeder.jar as a fatjar using src/v1feeder.java as the main class - Fire up an old-gen Qora node. -- Run ```src/migrate.java``` as a Java application to migrate old Qora blocks to DB. +- Use ```v1feeder.jar``` to migrate old Qora blocks to DB: -You should now be able to run ```src/test/load.java``` and ```src/test/save.java``` -as JUnit tests demonstrating loading/saving Transactions from/to database. +```java -jar v1feeder.jar qora-v1-node-ip``` + +You should now be able to run all the JUnit tests. You can also examine the migrated database using [HSQLDB's "sqltool"](http://www.hsqldb.org/doc/2.0/util-guide/sqltool-chapt.html). @@ -21,17 +23,23 @@ Typical command line for sqltool would be: rlwrap java -cp ${HSQLDB_JAR}:${SQLTOOL_JAR} org.hsqldb.cmdline.SqlTool --rcFile=${SQLTOOL_RC} qora ``` -```${HSQLDB_JAR}``` contains pathname to ```hsqldb-2.4.0.jar```, -typically ```${HOME}/.m2/repository/org/hsqldb/hsqldb/2.4.0/hsqldb-2.4.0.jar``` +```${HSQLDB_JAR}``` contains pathname to where Maven downloaded hsqldb, +typically ```${HOME}/.m2/repository/org/hsqldb/hsqldb/2.4.0/hsqldb-2.4.0.jar```, +but for now ```lib/org/hsqldb/hsqldb/r5836/hsqldb-r5836.jar``` -```${SQLTOOL_JAR}``` contains pathname to where you -[downloaded sqltool-2.2.6.jar](http://search.maven.org/remotecontent?filepath=org/hsqldb/sqltool/2.2.6/sqltool-2.2.6.jar) +```${SQLTOOL_JAR}``` contains pathname to where Maven downloaded sqltool, +typically ```${HOME}/.m2/repository/org/hsqldb/sqltool/2.4.1/sqltool-2.4.1.jar``` ```${SQLTOOL_RC}``` contains pathname to a text file describing Qora2 database, e.g. ```${HOME}/.sqltool.rc```, with contents like: ``` urlid qora +url jdbc:hsqldb:file:db/qora +username SA +password + +urlid qora-test url jdbc:hsqldb:file:db/test username SA password @@ -41,8 +49,10 @@ You could change the line ```url jdbc:hsqldb:file:db/test``` to use a full pathn Another idea is to assign a shell alias in your ```.bashrc``` like: ``` +export HSQLDB_JAR=${HOME}/.m2/repository/org/hsqldb/hsqldb/2.4.0/hsqldb-2.4.0.jar +export SQLTOOL_JAR=${HOME}/.m2/repository/org/hsqldb/sqltool/2.4.1/sqltool-2.4.1.jar alias sqltool='rlwrap java -cp ${HSQLDB_JAR}:${SQLTOOL_JAR} org.hsqldb.cmdline.SqlTool --rcFile=${SQLTOOL_RC}' ``` -So you can simply type: ```sqltool qora``` +So you can simply type: ```sqltool qora-test``` Don't forget to use ```SHUTDOWN;``` before exiting sqltool so that database files are closed cleanly. \ No newline at end of file diff --git a/lib/org/ciyam/at/1.0/at-1.0.jar b/lib/org/ciyam/at/1.0/at-1.0.jar new file mode 100644 index 00000000..1b554f34 Binary files /dev/null and b/lib/org/ciyam/at/1.0/at-1.0.jar differ diff --git a/lib/org/ciyam/at/1.0/at-1.0.jar.md5 b/lib/org/ciyam/at/1.0/at-1.0.jar.md5 new file mode 100644 index 00000000..c06cf86a --- /dev/null +++ b/lib/org/ciyam/at/1.0/at-1.0.jar.md5 @@ -0,0 +1 @@ +1d6f5d634a2c4e570a5a8af260a51653 \ No newline at end of file diff --git a/lib/org/ciyam/at/1.0/at-1.0.jar.sha1 b/lib/org/ciyam/at/1.0/at-1.0.jar.sha1 new file mode 100644 index 00000000..2f748fd6 --- /dev/null +++ b/lib/org/ciyam/at/1.0/at-1.0.jar.sha1 @@ -0,0 +1 @@ +c6387380bc5db1f0a98ecbb480b17bd89b564401 \ No newline at end of file diff --git a/lib/org/ciyam/at/1.0/at-1.0.pom b/lib/org/ciyam/at/1.0/at-1.0.pom new file mode 100644 index 00000000..05532457 --- /dev/null +++ b/lib/org/ciyam/at/1.0/at-1.0.pom @@ -0,0 +1,8 @@ + + + 4.0.0 + org.ciyam + at + 1.0 + diff --git a/lib/org/ciyam/at/1.0/at-1.0.pom.md5 b/lib/org/ciyam/at/1.0/at-1.0.pom.md5 new file mode 100644 index 00000000..811dfee8 --- /dev/null +++ b/lib/org/ciyam/at/1.0/at-1.0.pom.md5 @@ -0,0 +1 @@ +42f6e3eb3c6e510f65c963ce97583f05 \ No newline at end of file diff --git a/lib/org/ciyam/at/1.0/at-1.0.pom.sha1 b/lib/org/ciyam/at/1.0/at-1.0.pom.sha1 new file mode 100644 index 00000000..6b6618c9 --- /dev/null +++ b/lib/org/ciyam/at/1.0/at-1.0.pom.sha1 @@ -0,0 +1 @@ +490287647d3c69c05bd50ab565ffff86192ff423 \ No newline at end of file diff --git a/lib/org/ciyam/at/maven-metadata.xml b/lib/org/ciyam/at/maven-metadata.xml new file mode 100644 index 00000000..9b556545 --- /dev/null +++ b/lib/org/ciyam/at/maven-metadata.xml @@ -0,0 +1,12 @@ + + + org.ciyam + at + + 1.0 + + 1.0 + + 20181003154752 + + diff --git a/lib/org/ciyam/at/maven-metadata.xml.md5 b/lib/org/ciyam/at/maven-metadata.xml.md5 new file mode 100644 index 00000000..de530860 --- /dev/null +++ b/lib/org/ciyam/at/maven-metadata.xml.md5 @@ -0,0 +1 @@ +bc81bc1f9b74a4eececd5dd8b29e47d8 \ No newline at end of file diff --git a/lib/org/ciyam/at/maven-metadata.xml.sha1 b/lib/org/ciyam/at/maven-metadata.xml.sha1 new file mode 100644 index 00000000..1746aa6b --- /dev/null +++ b/lib/org/ciyam/at/maven-metadata.xml.sha1 @@ -0,0 +1 @@ +feefde4343bda4d6e13159e5c01f8b4f8963a1bc \ No newline at end of file diff --git a/log4j2.properties b/log4j2.properties new file mode 100644 index 00000000..187ef94a --- /dev/null +++ b/log4j2.properties @@ -0,0 +1,41 @@ +rootLogger.level = info +# On Windows, this might be rewritten as: +# property.filename = ${sys:user.home}\\AppData\\Roaming\\Qora\\log.txt +property.filename = log.txt + +rootLogger.appenderRef.console.ref = stdout +rootLogger.appenderRef.rolling.ref = FILE + +# Override HSQLDB logging level to "warn" as too much is logged at "info" +logger.hsqldb.name = hsqldb.db +logger.hsqldb.level = warn +logger.hsqldb.appenderRef.rolling.ref = FILE + +# Override logging level for this class +logger.voting.name = qora.transaction.VoteOnPollTransaction +logger.voting.level = trace +logger.voting.appenderRef.rolling.ref = FILE + +# Override logging level for this class +logger.assets.name = qora.assets.Order +logger.assets.level = trace +logger.assets.appenderRef.rolling.ref = FILE + +appender.console.type = Console +appender.console.name = stdout +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n +appender.console.filter.threshold.type = ThresholdFilter +appender.console.filter.threshold.level = error + +appender.rolling.type = RollingFile +appender.rolling.name = FILE +appender.rolling.layout.type = PatternLayout +appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n +appender.rolling.filePattern = ${filename}.%i +appender.rolling.policy.type = SizeBasedTriggeringPolicy +appender.rolling.policy.size = 4MB +# Set the immediate flush to true (default) +# appender.rolling.immediateFlush = true +# Set the append to true (default), should not overwrite +# appender.rolling.append=true diff --git a/pom.xml b/pom.xml index dfd08a3d..72da220b 100644 --- a/pom.xml +++ b/pom.xml @@ -107,5 +107,16 @@ jersey-media-moxy 2.27 + + org.ciyam + at + 1.0 + + + org.hsqldb + sqltool + 2.4.1 + test + \ No newline at end of file diff --git a/src/data/at/ATData.java b/src/data/at/ATData.java new file mode 100644 index 00000000..bf8272e6 --- /dev/null +++ b/src/data/at/ATData.java @@ -0,0 +1,110 @@ +package data.at; + +import java.math.BigDecimal; + +public class ATData { + + // Properties + private String ATAddress; + private int version; + private byte[] codeBytes; + private boolean isSleeping; + private Integer sleepUntilHeight; + private boolean isFinished; + private boolean hadFatalError; + private boolean isFrozen; + private BigDecimal frozenBalance; + private byte[] deploySignature; + + // Constructors + + public ATData(String ATAddress, int version, byte[] codeBytes, boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, + boolean isFrozen, BigDecimal frozenBalance, byte[] deploySignature) { + this.ATAddress = ATAddress; + this.version = version; + this.codeBytes = codeBytes; + this.isSleeping = isSleeping; + this.sleepUntilHeight = sleepUntilHeight; + this.isFinished = isFinished; + this.hadFatalError = hadFatalError; + this.isFrozen = isFrozen; + this.frozenBalance = frozenBalance; + this.deploySignature = deploySignature; + } + + public ATData(String ATAddress, int version, byte[] codeBytes, boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, + boolean isFrozen, Long frozenBalance, byte[] deploySignature) { + this(ATAddress, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, (BigDecimal) null, deploySignature); + + // Convert Long frozenBalance to BigDecimal + if (frozenBalance != null) + this.frozenBalance = BigDecimal.valueOf(frozenBalance).setScale(8).divide(BigDecimal.valueOf(1e8)); + } + + // Getters / setters + + public String getATAddress() { + return this.ATAddress; + } + + public int getVersion() { + return this.version; + } + + public byte[] getCodeBytes() { + return this.codeBytes; + } + + public boolean getIsSleeping() { + return this.isSleeping; + } + + public void setIsSleeping(boolean isSleeping) { + this.isSleeping = isSleeping; + } + + public Integer getSleepUntilHeight() { + return this.sleepUntilHeight; + } + + public void setSleepUntilHeight(Integer sleepUntilHeight) { + this.sleepUntilHeight = sleepUntilHeight; + } + + public boolean getIsFinished() { + return this.isFinished; + } + + public void setIsFinished(boolean isFinished) { + this.isFinished = isFinished; + } + + public boolean getHadFatalError() { + return this.hadFatalError; + } + + public void setHadFatalError(boolean hadFatalError) { + this.hadFatalError = hadFatalError; + } + + public boolean getIsFrozen() { + return this.isFrozen; + } + + public void setIsFrozen(boolean isFrozen) { + this.isFrozen = isFrozen; + } + + public BigDecimal getFrozenBalance() { + return this.frozenBalance; + } + + public void setFrozenBalance(BigDecimal frozenBalance) { + this.frozenBalance = frozenBalance; + } + + public byte[] getDeploySignature() { + return this.deploySignature; + } + +} diff --git a/src/data/at/ATStateData.java b/src/data/at/ATStateData.java new file mode 100644 index 00000000..192c8095 --- /dev/null +++ b/src/data/at/ATStateData.java @@ -0,0 +1,32 @@ +package data.at; + +public class ATStateData { + + // Properties + private String ATAddress; + private int height; + private byte[] stateData; + + // Constructors + + public ATStateData(String ATAddress, int height, byte[] stateData) { + this.ATAddress = ATAddress; + this.height = height; + this.stateData = stateData; + } + + // Getters / setters + + public String getATAddress() { + return this.ATAddress; + } + + public int getHeight() { + return this.height; + } + + public byte[] getStateData() { + return this.stateData; + } + +} diff --git a/src/data/transaction/ATTransactionData.java b/src/data/transaction/ATTransactionData.java new file mode 100644 index 00000000..43053561 --- /dev/null +++ b/src/data/transaction/ATTransactionData.java @@ -0,0 +1,49 @@ +package data.transaction; + +import java.math.BigDecimal; + +import qora.transaction.Transaction.TransactionType; + +public class ATTransactionData extends TransactionData { + + // Properties + private byte[] senderPublicKey; + private String recipient; + private BigDecimal amount; + private byte[] message; + + // Constructors + + public ATTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, byte[] message, BigDecimal fee, long timestamp, byte[] reference, + byte[] signature) { + super(TransactionType.AT, fee, senderPublicKey, timestamp, reference, signature); + + this.senderPublicKey = senderPublicKey; + this.recipient = recipient; + this.amount = amount; + this.message = message; + } + + public ATTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, byte[] message, BigDecimal fee, long timestamp, byte[] reference) { + this(senderPublicKey, recipient, amount, message, fee, timestamp, reference, null); + } + + // Getters/Setters + + public byte[] getSenderPublicKey() { + return this.senderPublicKey; + } + + public String getRecipient() { + return this.recipient; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public byte[] getMessage() { + return this.message; + } + +} diff --git a/src/data/transaction/ArbitraryTransactionData.java b/src/data/transaction/ArbitraryTransactionData.java index a5de2238..7392e1ff 100644 --- a/src/data/transaction/ArbitraryTransactionData.java +++ b/src/data/transaction/ArbitraryTransactionData.java @@ -10,7 +10,8 @@ public class ArbitraryTransactionData extends TransactionData { // "data" field types public enum DataType { - RAW_DATA, DATA_HASH; + RAW_DATA, + DATA_HASH; } // Properties diff --git a/src/data/transaction/DeployATTransactionData.java b/src/data/transaction/DeployATTransactionData.java new file mode 100644 index 00000000..101e2094 --- /dev/null +++ b/src/data/transaction/DeployATTransactionData.java @@ -0,0 +1,77 @@ +package data.transaction; + +import java.math.BigDecimal; + +import qora.transaction.Transaction.TransactionType; + +public class DeployATTransactionData extends TransactionData { + + // Properties + private String name; + private String description; + private String ATType; + private String tags; + private byte[] creationBytes; + private BigDecimal amount; + 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) { + super(TransactionType.DEPLOY_AT, fee, creatorPublicKey, timestamp, reference, signature); + + this.name = name; + this.description = description; + this.ATType = ATType; + this.tags = tags; + this.amount = amount; + 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); + } + + 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); + } + + // Getters/Setters + + public String getName() { + return this.name; + } + + public String getDescription() { + return this.description; + } + + public String getATType() { + return this.ATType; + } + + public String getTags() { + return this.tags; + } + + public byte[] getCreationBytes() { + return this.creationBytes; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public String getATAddress() { + return this.ATAddress; + } + + public void setATAddress(String ATAddress) { + this.ATAddress = ATAddress; + } + +} diff --git a/src/qora/at/AT.java b/src/qora/at/AT.java new file mode 100644 index 00000000..8c1a9b99 --- /dev/null +++ b/src/qora/at/AT.java @@ -0,0 +1,52 @@ +package qora.at; + +import org.ciyam.at.MachineState; + +import data.at.ATData; +import data.at.ATStateData; +import data.transaction.DeployATTransactionData; +import repository.DataException; +import repository.Repository; + +public class AT { + + // Properties + private Repository repository; + private ATData atData; + private ATStateData atStateData; + + // Constructors + + public AT(Repository repository, ATData atData, ATStateData atStateData) { + this.repository = repository; + this.atData = atData; + this.atStateData = atStateData; + } + + public AT(Repository repository, DeployATTransactionData deployATTransactionData) throws DataException { + this.repository = repository; + + MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes()); + + this.atData = new ATData(deployATTransactionData.getATAddress(), machineState.version, machineState.codeByteBuffer.array(), machineState.isSleeping, + machineState.sleepUntilHeight, machineState.isFinished, machineState.hadFatalError, machineState.isFrozen, machineState.frozenBalance, + deployATTransactionData.getSignature()); + + String atAddress = this.atData.getATAddress(); + + int height = this.repository.getBlockRepository().getBlockchainHeight(); + byte[] stateData = machineState.toBytes(); + + this.atStateData = new ATStateData(atAddress, height, stateData); + } + + public void deploy() throws DataException { + this.repository.getATRepository().save(this.atData); + this.repository.getATRepository().save(this.atStateData); + } + + public void undeploy() throws DataException { + // AT states deleted implicitly by repository + this.repository.getATRepository().delete(this.atData.getATAddress()); + } +} diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index 22c49bf6..5dc2340f 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -60,9 +60,21 @@ public class Block { // Validation results public enum ValidationResult { - OK(1), REFERENCE_MISSING(10), PARENT_DOES_NOT_EXIST(11), BLOCKCHAIN_NOT_EMPTY(12), TIMESTAMP_OLDER_THAN_PARENT(20), TIMESTAMP_IN_FUTURE( - 21), TIMESTAMP_MS_INCORRECT(22), VERSION_INCORRECT(30), FEATURE_NOT_YET_RELEASED(31), GENERATING_BALANCE_INCORRECT(40), GENERATOR_NOT_ACCEPTED( - 41), GENESIS_TRANSACTIONS_INVALID(50), TRANSACTION_TIMESTAMP_INVALID(51), TRANSACTION_INVALID(52), TRANSACTION_PROCESSING_FAILED(53); + OK(1), + REFERENCE_MISSING(10), + PARENT_DOES_NOT_EXIST(11), + BLOCKCHAIN_NOT_EMPTY(12), + TIMESTAMP_OLDER_THAN_PARENT(20), + TIMESTAMP_IN_FUTURE(21), + TIMESTAMP_MS_INCORRECT(22), + VERSION_INCORRECT(30), + FEATURE_NOT_YET_RELEASED(31), + GENERATING_BALANCE_INCORRECT(40), + GENERATOR_NOT_ACCEPTED(41), + GENESIS_TRANSACTIONS_INVALID(50), + TRANSACTION_TIMESTAMP_INVALID(51), + TRANSACTION_INVALID(52), + TRANSACTION_PROCESSING_FAILED(53); public final int value; @@ -123,6 +135,7 @@ public class Block { this.transactions = new ArrayList(); } + /** Construct a new block for use in tests */ public Block(Repository repository, BlockData parentBlockData, PrivateKeyAccount generator, byte[] atBytes, BigDecimal atFees) throws DataException { this.repository = repository; this.generator = generator; @@ -131,13 +144,20 @@ public class Block { int version = parentBlock.getNextBlockVersion(); byte[] reference = parentBlockData.getSignature(); - long timestamp = parentBlock.calcNextBlockTimestamp(generator); BigDecimal generatingBalance = parentBlock.calcNextBlockGeneratingBalance(); - this.blockData = new BlockData(version, reference, 0, BigDecimal.ZERO.setScale(8), null, 0, timestamp, generatingBalance, generator.getPublicKey(), - null, atBytes, atFees); + byte[] generatorSignature; + try { + generatorSignature = generator + .sign(BlockTransformer.getBytesForGeneratorSignature(parentBlockData.getGeneratorSignature(), generatingBalance, generator)); + } catch (TransformationException e) { + throw new DataException("Unable to calculate next block generator signature", e); + } - calcGeneratorSignature(); + long timestamp = parentBlock.calcNextBlockTimestamp(version, generatorSignature, generator); + + this.blockData = new BlockData(version, reference, 0, BigDecimal.ZERO.setScale(8), null, 0, timestamp, generatingBalance, generator.getPublicKey(), + generatorSignature, atBytes, atFees); this.transactions = new ArrayList(); } @@ -288,8 +308,23 @@ public class Block { return new BigInteger(1, hash); } - private long calcNextBlockTimestamp(Account nextBlockGenerator) throws DataException { - BigInteger hashValue = calcBlockHash(); + private BigInteger calcNextBlockHash(int nextBlockVersion, byte[] preVersion3GeneratorSignature, PublicKeyAccount nextBlockGenerator) { + byte[] hashData; + + if (nextBlockVersion < 3) + hashData = preVersion3GeneratorSignature; + else + hashData = Bytes.concat(this.blockData.getSignature(), nextBlockGenerator.getPublicKey()); + + // Calculate 32-byte hash as pseudo-random, but deterministic, integer (unique to this generator for v3+ blocks) + byte[] hash = Crypto.digest(hashData); + + // Convert hash to BigInteger form + return new BigInteger(1, hash); + } + + private long calcNextBlockTimestamp(int nextBlockVersion, byte[] nextBlockGeneratorSignature, PrivateKeyAccount nextBlockGenerator) throws DataException { + BigInteger hashValue = calcNextBlockHash(nextBlockVersion, nextBlockGeneratorSignature, nextBlockGenerator); BigInteger target = calcGeneratorsTarget(nextBlockGenerator); // If target is zero then generator has no balance so return longest value @@ -417,6 +452,12 @@ public class Block { * Recalculate block's generator signature. *

* Requires block's {@code generator} being a {@code PrivateKeyAccount}. + *

+ * Generator signature is made by the generator signing the following data: + *

+ * previous block's generator signature + this block's generating balance + generator's public key + *

+ * (Previous block's generator signature is extracted from this block's reference). * * @throws IllegalStateException * if block's {@code generator} is not a {@code PrivateKeyAccount}. @@ -538,11 +579,12 @@ public class Block { return ValidationResult.GENERATOR_NOT_ACCEPTED; // XXX Odd gen1 test: "CHECK IF FIRST BLOCK OF USER" - // Is the comment wrong and this each second elapsed allows generator to test a new "target" window against hashValue? + // Is the comment wrong? Does each second elapsed allows generator to test a new "target" window against hashValue? if (hashValue.compareTo(lowerTarget) < 0) return ValidationResult.GENERATOR_NOT_ACCEPTED; - // Check CIYAM AT + // Process CIYAM ATs, prepending AT-Transactions to block then compare post-execution checksums + // XXX We should pre-calculate, and cache, next block's AT-transactions after processing each block to save repeated work if (this.blockData.getAtBytes() != null && this.blockData.getAtBytes().length > 0) { // TODO // try { @@ -591,8 +633,8 @@ public class Block { this.repository.discardChanges(); } catch (DataException e) { /* - * Discard failure most likely due to prior DataException, so catch discardChanges' DataException and discard. Prior DataException propagates to - * caller. Successful completion of try-block continues on after discard. + * discardChanges failure most likely due to prior DataException, so catch discardChanges' DataException and ignore. Prior DataException + * propagates to caller. */ } } diff --git a/src/qora/block/BlockChain.java b/src/qora/block/BlockChain.java index 3a2b4074..a35ca067 100644 --- a/src/qora/block/BlockChain.java +++ b/src/qora/block/BlockChain.java @@ -43,6 +43,7 @@ public class BlockChain { 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 /** * Some sort start-up/initialization/checking method. @@ -173,4 +174,11 @@ public class BlockChain { return ARBITRARY_TRANSACTION_V2_TIMESTAMP; } + public static long getDeployATV2Timestamp() { + if (Settings.getInstance().isTestNet()) + return 0; + + return DEPLOY_AT_V2_TIMESTAMP; + } + } diff --git a/src/qora/transaction/DeployATTransaction.java b/src/qora/transaction/DeployATTransaction.java new file mode 100644 index 00000000..45d77600 --- /dev/null +++ b/src/qora/transaction/DeployATTransaction.java @@ -0,0 +1,217 @@ +package qora.transaction; + +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.google.common.base.Utf8; + +import data.transaction.DeployATTransactionData; +import data.transaction.TransactionData; +import qora.account.Account; +import qora.assets.Asset; +import qora.at.AT; +import qora.block.BlockChain; +import qora.crypto.Crypto; +import repository.DataException; +import repository.Repository; +import transform.Transformer; + +public class DeployATTransaction extends Transaction { + + // Properties + private DeployATTransactionData deployATTransactionData; + + // Other useful constants + public static final int MAX_NAME_SIZE = 200; + public static final int MAX_DESCRIPTION_SIZE = 2000; + public static final int MAX_AT_TYPE_SIZE = 200; + public static final int MAX_TAGS_SIZE = 200; + public static final int MAX_CREATION_BYTES_SIZE = 100_000; + + // Constructors + + public DeployATTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + + this.deployATTransactionData = (DeployATTransactionData) this.transactionData; + } + + // More information + + @Override + public List getRecipientAccounts() throws DataException { + return new ArrayList(); + } + + @Override + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getCreator().getAddress())) + return true; + + if (address.equals(this.getATAccount().getAddress())) + return true; + + return false; + } + + @Override + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + + if (address.equals(this.getCreator().getAddress())) + amount = amount.subtract(this.deployATTransactionData.getAmount()).subtract(this.transactionData.getFee()); + + if (address.equals(this.getATAccount().getAddress())) + amount = amount.add(this.deployATTransactionData.getAmount()); + + return amount; + } + + /** Make sure deployATTransactionData has an ATAddress */ + private void ensureATAddress() throws DataException { + if (this.deployATTransactionData.getATAddress() != null) + return; + + int blockHeight = this.getHeight(); + if (blockHeight == 0) + blockHeight = this.repository.getBlockRepository().getBlockchainHeight(); + + try { + byte[] name = this.deployATTransactionData.getName().getBytes("UTF-8"); + byte[] description = this.deployATTransactionData.getDescription().replaceAll("\\s", "").getBytes("UTF-8"); + byte[] creatorPublicKey = this.deployATTransactionData.getCreatorPublicKey(); + byte[] creationBytes = this.deployATTransactionData.getCreationBytes(); + + ByteBuffer byteBuffer = ByteBuffer + .allocate(name.length + description.length + creatorPublicKey.length + creationBytes.length + Transformer.INT_LENGTH); + + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + byteBuffer.put(name); + byteBuffer.put(description); + byteBuffer.put(creatorPublicKey); + byteBuffer.put(creationBytes); + byteBuffer.putInt(blockHeight); + + String atAddress = Crypto.toATAddress(byteBuffer.array()); + + this.deployATTransactionData.setATAddress(atAddress); + } catch (UnsupportedEncodingException e) { + throw new DataException("Unable to generate AT account from Deploy AT transaction data", e); + } + } + + // Navigation + + public Account getATAccount() throws DataException { + ensureATAddress(); + + return new Account(this.repository, this.deployATTransactionData.getATAddress()); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + if (this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.getATReleaseHeight()) + return ValidationResult.NOT_YET_RELEASED; + + // Check name size bounds + int nameLength = Utf8.encodedLength(deployATTransactionData.getName()); + if (nameLength < 1 || nameLength > MAX_NAME_SIZE) + return ValidationResult.INVALID_NAME_LENGTH; + + // Check description size bounds + int descriptionlength = Utf8.encodedLength(deployATTransactionData.getDescription()); + if (descriptionlength < 1 || descriptionlength > MAX_DESCRIPTION_SIZE) + return ValidationResult.INVALID_DESCRIPTION_LENGTH; + + // Check AT-type size bounds + int ATTypeLength = Utf8.encodedLength(deployATTransactionData.getATType()); + if (ATTypeLength < 1 || ATTypeLength > MAX_AT_TYPE_SIZE) + return ValidationResult.INVALID_AT_TYPE_LENGTH; + + // Check tags size bounds + int tagsLength = Utf8.encodedLength(deployATTransactionData.getTags()); + if (tagsLength < 1 || tagsLength > MAX_TAGS_SIZE) + return ValidationResult.INVALID_TAGS_LENGTH; + + // Check amount is positive + if (deployATTransactionData.getAmount().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_AMOUNT; + + // Check fee is positive + if (deployATTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_FEE; + + // Check reference is correct + Account creator = getCreator(); + + if (!Arrays.equals(creator.getLastReference(), deployATTransactionData.getReference())) + 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; + + // Check creation bytes are valid (for v3+) + byte[] creationBytes = deployATTransactionData.getCreationBytes(); + short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian + + if (version >= 3) { + // Do actual validation + } else { + // Skip validation for old, dead ATs + } + + return ValidationResult.OK; + } + + @Override + public void process() throws DataException { + ensureATAddress(); + + // Deploy AT, saving into repository + AT at = new AT(this.repository, this.deployATTransactionData); + at.deploy(); + + // Save this transaction itself + this.repository.getTransactionRepository().save(this.transactionData); + + // Update creator's balance + Account creator = getCreator(); + creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(deployATTransactionData.getAmount())); + creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).subtract(deployATTransactionData.getFee())); + + // Update creator's reference + creator.setLastReference(deployATTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + // Delete AT from repository + AT at = new AT(this.repository, this.deployATTransactionData); + at.undeploy(); + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(deployATTransactionData); + + // Update creator's balance + Account creator = getCreator(); + creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(deployATTransactionData.getAmount())); + creator.setConfirmedBalance(Asset.QORA, creator.getConfirmedBalance(Asset.QORA).add(deployATTransactionData.getFee())); + + // Update creator's reference + creator.setLastReference(deployATTransactionData.getReference()); + } + +} diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java index ed2330b8..d3ae1bc1 100644 --- a/src/qora/transaction/GenesisTransaction.java +++ b/src/qora/transaction/GenesisTransaction.java @@ -118,7 +118,7 @@ public class GenesisTransaction extends Transaction { @Override public ValidationResult isValid() { // Check amount is zero or positive - if (genesisTransactionData.getAmount().compareTo(BigDecimal.ZERO) >= 0) + if (genesisTransactionData.getAmount().compareTo(BigDecimal.ZERO) < 0) return ValidationResult.NEGATIVE_AMOUNT; // Check recipient address is valid diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 42b7c418..aaba8836 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -24,8 +24,27 @@ public abstract class Transaction { // Transaction types public enum TransactionType { - GENESIS(1), PAYMENT(2), REGISTER_NAME(3), UPDATE_NAME(4), SELL_NAME(5), CANCEL_SELL_NAME(6), BUY_NAME(7), CREATE_POLL(8), VOTE_ON_POLL(9), ARBITRARY( - 10), ISSUE_ASSET(11), TRANSFER_ASSET(12), CREATE_ASSET_ORDER(13), CANCEL_ASSET_ORDER(14), MULTIPAYMENT(15), DEPLOY_AT(16), MESSAGE(17); + GENESIS(1), + PAYMENT(2), + REGISTER_NAME(3), + UPDATE_NAME(4), + SELL_NAME(5), + CANCEL_SELL_NAME(6), + BUY_NAME(7), + CREATE_POLL(8), + VOTE_ON_POLL(9), + ARBITRARY(10), + ISSUE_ASSET(11), + TRANSFER_ASSET(12), + CREATE_ASSET_ORDER(13), + CANCEL_ASSET_ORDER(14), + MULTIPAYMENT(15), + DEPLOY_AT(16), + MESSAGE(17), + DELEGATION(18), + SUPERNODE(19), + AIRDROP(20), + AT(21); public final int value; @@ -42,14 +61,44 @@ public abstract class Transaction { // Validation results public enum ValidationResult { - OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6), INVALID_NAME_LENGTH(7), INVALID_VALUE_LENGTH( - 8), NAME_ALREADY_REGISTERED(9), NAME_DOES_NOT_EXIST(10), INVALID_NAME_OWNER(11), NAME_ALREADY_FOR_SALE(12), NAME_NOT_FOR_SALE( - 13), BUYER_ALREADY_OWNER(14), INVALID_AMOUNT(15), INVALID_SELLER(16), NAME_NOT_LOWER_CASE(17), INVALID_DESCRIPTION_LENGTH( - 18), INVALID_OPTIONS_COUNT(19), INVALID_OPTION_LENGTH(20), DUPLICATE_OPTION(21), POLL_ALREADY_EXISTS(22), POLL_DOES_NOT_EXIST( - 24), POLL_OPTION_DOES_NOT_EXIST(25), ALREADY_VOTED_FOR_THAT_OPTION(26), INVALID_DATA_LENGTH(27), INVALID_QUANTITY( - 28), ASSET_DOES_NOT_EXIST(29), INVALID_RETURN(30), HAVE_EQUALS_WANT(31), ORDER_DOES_NOT_EXIST( - 32), INVALID_ORDER_CREATOR(33), INVALID_PAYMENTS_COUNT( - 34), NEGATIVE_PRICE(35), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000); + OK(1), + INVALID_ADDRESS(2), + NEGATIVE_AMOUNT(3), + NEGATIVE_FEE(4), + NO_BALANCE(5), + INVALID_REFERENCE(6), + INVALID_NAME_LENGTH(7), + INVALID_VALUE_LENGTH(8), + NAME_ALREADY_REGISTERED(9), + NAME_DOES_NOT_EXIST(10), + INVALID_NAME_OWNER(11), + NAME_ALREADY_FOR_SALE(12), + NAME_NOT_FOR_SALE(13), + BUYER_ALREADY_OWNER(14), + INVALID_AMOUNT(15), + INVALID_SELLER(16), + NAME_NOT_LOWER_CASE(17), + INVALID_DESCRIPTION_LENGTH(18), + INVALID_OPTIONS_COUNT(19), + INVALID_OPTION_LENGTH(20), + DUPLICATE_OPTION(21), + POLL_ALREADY_EXISTS(22), + POLL_DOES_NOT_EXIST(24), + POLL_OPTION_DOES_NOT_EXIST(25), + ALREADY_VOTED_FOR_THAT_OPTION(26), + INVALID_DATA_LENGTH(27), + INVALID_QUANTITY(28), + ASSET_DOES_NOT_EXIST(29), + INVALID_RETURN(30), + HAVE_EQUALS_WANT(31), + ORDER_DOES_NOT_EXIST(32), + INVALID_ORDER_CREATOR(33), + INVALID_PAYMENTS_COUNT(34), + NEGATIVE_PRICE(35), + INVALID_TAGS_LENGTH(37), + INVALID_AT_TYPE_LENGTH(38), + ASSET_ALREADY_EXISTS(43), + NOT_YET_RELEASED(1000); public final int value; @@ -147,6 +196,9 @@ public abstract class Transaction { case MESSAGE: return new MessageTransaction(repository, transactionData); + case DEPLOY_AT: + return new DeployATTransaction(repository, transactionData); + default: throw new IllegalStateException("Unsupported transaction type [" + transactionData.getType().value + "] during fetch from repository"); } diff --git a/src/qora/transaction/TransferAssetTransaction.java b/src/qora/transaction/TransferAssetTransaction.java index 40bda07b..5325c9f0 100644 --- a/src/qora/transaction/TransferAssetTransaction.java +++ b/src/qora/transaction/TransferAssetTransaction.java @@ -84,9 +84,9 @@ public class TransferAssetTransaction extends Transaction { @Override public ValidationResult isValid() throws DataException { - // Are IssueAssetTransactions even allowed at this point? + // Are TransferAssetTransactions even allowed at this point? // XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used? - if (this.transferAssetTransactionData.getTimestamp() < BlockChain.getVotingReleaseTimestamp()) + if (this.transferAssetTransactionData.getTimestamp() < BlockChain.getAssetsReleaseTimestamp()) return ValidationResult.NOT_YET_RELEASED; // Check reference is correct diff --git a/src/repository/ATRepository.java b/src/repository/ATRepository.java new file mode 100644 index 00000000..fef0c76e --- /dev/null +++ b/src/repository/ATRepository.java @@ -0,0 +1,24 @@ +package repository; + +import data.at.ATData; +import data.at.ATStateData; + +public interface ATRepository { + + // CIYAM AutomatedTransactions + + public ATData fromATAddress(String atAddress) throws DataException; + + public void save(ATData atData) throws DataException; + + public void delete(String atAddress) throws DataException; + + // AT States + + public ATStateData getATState(String atAddress, int height) throws DataException; + + public void save(ATStateData atStateData) throws DataException; + + public void delete(String atAddress, int height) throws DataException; + +} diff --git a/src/repository/Repository.java b/src/repository/Repository.java index fa969947..f5bc16ba 100644 --- a/src/repository/Repository.java +++ b/src/repository/Repository.java @@ -2,6 +2,8 @@ package repository; public interface Repository extends AutoCloseable { + public ATRepository getATRepository(); + public AccountRepository getAccountRepository(); public AssetRepository getAssetRepository(); diff --git a/src/repository/hsqldb/HSQLDBATRepository.java b/src/repository/hsqldb/HSQLDBATRepository.java new file mode 100644 index 00000000..5c2cc88d --- /dev/null +++ b/src/repository/hsqldb/HSQLDBATRepository.java @@ -0,0 +1,117 @@ +package repository.hsqldb; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import data.at.ATData; +import data.at.ATStateData; +import repository.ATRepository; +import repository.DataException; + +public class HSQLDBATRepository implements ATRepository { + + protected HSQLDBRepository repository; + + public HSQLDBATRepository(HSQLDBRepository repository) { + this.repository = repository; + } + + // ATs + + @Override + public ATData fromATAddress(String atAddress) throws DataException { + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT owner, asset_name, description, quantity, is_divisible, reference FROM Assets WHERE AT_address = ?", atAddress)) { + if (resultSet == null) + return null; + + int version = resultSet.getInt(1); + byte[] codeBytes = resultSet.getBytes(2); // Actually BLOB + boolean isSleeping = resultSet.getBoolean(3); + + Integer sleepUntilHeight = resultSet.getInt(4); + if (resultSet.wasNull()) + sleepUntilHeight = null; + + boolean isFinished = resultSet.getBoolean(5); + boolean hadFatalError = resultSet.getBoolean(6); + boolean isFrozen = resultSet.getBoolean(7); + + BigDecimal frozenBalance = resultSet.getBigDecimal(8); + if (resultSet.wasNull()) + frozenBalance = null; + + byte[] deploySignature = resultSet.getBytes(9); + + return new ATData(atAddress, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance, deploySignature); + } catch (SQLException e) { + throw new DataException("Unable to fetch AT from repository", e); + } + } + + @Override + public void save(ATData atData) throws DataException { + HSQLDBSaver saveHelper = new HSQLDBSaver("ATs"); + + saveHelper.bind("AT_address", atData.getATAddress()).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("deploy_signature", atData.getDeploySignature()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save AT into repository", e); + } + } + + @Override + public void delete(String atAddress) throws DataException { + try { + this.repository.delete("ATs", "atAddress = ?", atAddress); + // AT States also deleted via ON DELETE CASCADE + } catch (SQLException e) { + throw new DataException("Unable to delete AT from repository", e); + } + } + + // AT State + + @Override + public ATStateData getATState(String atAddress, int height) throws DataException { + try (ResultSet resultSet = this.repository.checkedExecute("SELECT state_data FROM ATStates WHERE AT_address = ? AND height = ?", atAddress, height)) { + if (resultSet == null) + return null; + + byte[] stateData = resultSet.getBytes(1); // Actually BLOB + + return new ATStateData(atAddress, height, stateData); + } catch (SQLException e) { + throw new DataException("Unable to fetch AT State from repository", e); + } + } + + @Override + public void save(ATStateData atStateData) throws DataException { + HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates"); + + saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight()).bind("state_data", atStateData.getStateData()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save AT State into repository", e); + } + } + + @Override + public void delete(String atAddress, int height) throws DataException { + try { + this.repository.delete("ATStates", "AT_address = ? AND height = ?", atAddress, height); + } catch (SQLException e) { + throw new DataException("Unable to delete AT State from repository", e); + } + } + +} diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java index 836b2a1e..f3f1dba7 100644 --- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -77,7 +77,8 @@ public class HSQLDBDatabaseUpdates { stmt.execute("SET DATABASE COLLATION SQL_TEXT NO PAD"); stmt.execute("CREATE COLLATION SQL_TEXT_UCC_NO_PAD FOR SQL_TEXT FROM SQL_TEXT_UCC NO PAD"); stmt.execute("CREATE COLLATION SQL_TEXT_NO_PAD FOR SQL_TEXT FROM SQL_TEXT NO PAD"); - stmt.execute("SET FILES SPACE TRUE"); + stmt.execute("SET FILES SPACE TRUE"); // Enable per-table block space within .data file, useful for CACHED table types + stmt.execute("SET FILES LOB SCALE 1"); // LOB granularity is 1KB stmt.execute("CREATE TABLE DatabaseInfo ( version INTEGER NOT NULL )"); stmt.execute("INSERT INTO DatabaseInfo VALUES ( 0 )"); stmt.execute("CREATE TYPE BlockSignature AS VARBINARY(128)"); @@ -96,6 +97,9 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CREATE TYPE AssetOrderID AS VARBINARY(64)"); stmt.execute("CREATE TYPE ATName AS VARCHAR(200) COLLATE SQL_TEXT_UCC_NO_PAD"); stmt.execute("CREATE TYPE ATType AS VARCHAR(200) COLLATE SQL_TEXT_UCC_NO_PAD"); + stmt.execute("CREATE TYPE ATCode AS BLOB(64K)"); // 16bit * 1 + stmt.execute("CREATE TYPE ATState AS BLOB(1M)"); // 16bit * 8 + 16bit * 4 + 16bit * 4 + stmt.execute("CREATE TYPE ATMessage AS VARBINARY(256)"); break; case 1: @@ -269,7 +273,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, " + + "creation_bytes VARBINARY(100000) NOT NULL, amount QoraAmount NOT NULL, AT_address QoraAddress, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); break; @@ -346,6 +350,22 @@ public class HSQLDBDatabaseUpdates { + "PRIMARY KEY (name))"); break; + case 27: + // CIYAM Automated Transactions + stmt.execute("CREATE TABLE ATs (AT_address QoraAddress, version INTEGER 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, deploy_signature Signature NOT NULL, PRIMARY key (AT_address))"); + // For finding executable ATs + stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, AT_address)"); + // AT state on a per-block basis + stmt.execute("CREATE TABLE ATStates (AT_address QoraAddress, height INTEGER NOT NULL, state_data ATState, " + + "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)"); + // Generated AT Transactions + stmt.execute( + "CREATE TABLE ATTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress, amount QoraAmount NOT NULL, message ATMessage, " + + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + break; + default: // nothing to do return false; diff --git a/src/repository/hsqldb/HSQLDBRepository.java b/src/repository/hsqldb/HSQLDBRepository.java index 3081ee55..fc181eea 100644 --- a/src/repository/hsqldb/HSQLDBRepository.java +++ b/src/repository/hsqldb/HSQLDBRepository.java @@ -11,6 +11,7 @@ import java.util.TimeZone; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import repository.ATRepository; import repository.AccountRepository; import repository.AssetRepository; import repository.BlockRepository; @@ -34,6 +35,11 @@ public class HSQLDBRepository implements Repository { this.connection = connection; } + @Override + public ATRepository getATRepository() { + return new HSQLDBATRepository(this); + } + @Override public AccountRepository getAccountRepository() { return new HSQLDBAccountRepository(this); @@ -84,6 +90,12 @@ public class HSQLDBRepository implements Repository { @Override public void close() throws DataException { + // Already closed? No need to do anything but maybe report double-call + if (this.connection == null) { + LOGGER.warn("HSQLDBRepository.close() called when repository already closed", new Exception("Repository already closed")); + return; + } + try (Statement stmt = this.connection.createStatement()) { // Diagnostic check for uncommitted changes if (!stmt.execute("SELECT transaction, transaction_size FROM information_schema.system_sessions")) // TRANSACTION_SIZE() broken? @@ -96,7 +108,7 @@ public class HSQLDBRepository implements Repository { boolean inTransaction = resultSet.getBoolean(1); int transactionCount = resultSet.getInt(2); if (inTransaction && transactionCount != 0) - LOGGER.warn("Uncommitted changes (" + transactionCount + ") during repository close"); + LOGGER.warn("Uncommitted changes (" + transactionCount + ") during repository close", new Exception("Uncommitted repository changes")); } // give connection back to the pool diff --git a/src/repository/hsqldb/transaction/HSQLDBDeployATTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBDeployATTransactionRepository.java new file mode 100644 index 00000000..a4d54d41 --- /dev/null +++ b/src/repository/hsqldb/transaction/HSQLDBDeployATTransactionRepository.java @@ -0,0 +1,63 @@ +package repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; + +import data.transaction.DeployATTransactionData; +import data.transaction.TransactionData; +import repository.DataException; +import repository.hsqldb.HSQLDBRepository; +import repository.hsqldb.HSQLDBSaver; + +public class HSQLDBDeployATTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBDeployATTransactionRepository(HSQLDBRepository repository) { + this.repository = repository; + } + + 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)) { + if (resultSet == null) + return null; + + String name = resultSet.getString(1); + String description = resultSet.getString(2); + String ATType = resultSet.getString(3); + String tags = resultSet.getString(4); + byte[] creationBytes = resultSet.getBytes(5); + BigDecimal amount = resultSet.getBigDecimal(6).setScale(8); + + // Special null-checking for AT address + String ATAddress = resultSet.getString(7); + if (resultSet.wasNull()) + ATAddress = null; + + return new DeployATTransactionData(ATAddress, creatorPublicKey, name, description, ATType, tags, creationBytes, amount, fee, timestamp, reference, + signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch deploy AT transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("DeployATTransactions"); + + saveHelper.bind("signature", deployATTransactionData.getSignature()).bind("creator", deployATTransactionData.getCreatorPublicKey()) + .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()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save deploy AT transaction into repository", e); + } + } + +} diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index f6d7fc59..316ee6a7 100644 --- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -35,6 +35,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { private HSQLDBCreateOrderTransactionRepository createOrderTransactionRepository; private HSQLDBCancelOrderTransactionRepository cancelOrderTransactionRepository; private HSQLDBMultiPaymentTransactionRepository multiPaymentTransactionRepository; + private HSQLDBDeployATTransactionRepository deployATTransactionRepository; private HSQLDBMessageTransactionRepository messageTransactionRepository; public HSQLDBTransactionRepository(HSQLDBRepository repository) { @@ -54,6 +55,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.createOrderTransactionRepository = new HSQLDBCreateOrderTransactionRepository(repository); this.cancelOrderTransactionRepository = new HSQLDBCancelOrderTransactionRepository(repository); this.multiPaymentTransactionRepository = new HSQLDBMultiPaymentTransactionRepository(repository); + this.deployATTransactionRepository = new HSQLDBDeployATTransactionRepository(repository); this.messageTransactionRepository = new HSQLDBMessageTransactionRepository(repository); } @@ -146,6 +148,9 @@ public class HSQLDBTransactionRepository implements TransactionRepository { case MULTIPAYMENT: return this.multiPaymentTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case DEPLOY_AT: + return this.deployATTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case MESSAGE: return this.messageTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); @@ -304,6 +309,10 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.multiPaymentTransactionRepository.save(transactionData); break; + case DEPLOY_AT: + this.deployATTransactionRepository.save(transactionData); + break; + case MESSAGE: this.messageTransactionRepository.save(transactionData); break; diff --git a/src/test/ATTests.java b/src/test/ATTests.java new file mode 100644 index 00000000..076bb927 --- /dev/null +++ b/src/test/ATTests.java @@ -0,0 +1,96 @@ +package test; + +import static org.junit.Assert.*; + +import java.math.BigDecimal; +import java.util.Arrays; + +import org.junit.Test; + +import com.google.common.hash.HashCode; + +import data.block.BlockData; +import data.block.BlockTransactionData; +import data.transaction.DeployATTransactionData; +import qora.transaction.DeployATTransaction; +import repository.DataException; +import repository.Repository; +import repository.RepositoryManager; +import transform.TransformationException; +import utils.Base58; + +public class ATTests extends Common { + + @Test + public void testATAccount() throws TransformationException, DataException { + // 2dZ4megUyNoYYY7qWmuSd4xw1yUKgPPF97yBbeddh8aKuC8PLpz7Xvf3r6Zjv1zwGrR8fEAHuaztCPD4KQp76KdL at height 125598 + // AT address: AaaUn82XV4YcUtsQ3rHa5ZgqyiK35rVfE3 + + String expectedAddress = "AaaUn82XV4YcUtsQ3rHa5ZgqyiK35rVfE3"; + + byte[] creatorPublicKey = HashCode.fromString("c74d71ecec6b37890f26573186e634986cc90a507af01749f92aa2c7c95ad05f").asBytes(); + String name = "QORABURST @ 1.00"; + String description = "Initiators BURST address: BURST-LKGW-Z2JK-EZ99-E7CUE"; + String ATType = "acct"; + String tags = "acct,atomic cross chain tx,initiate,initiator"; + byte[] creationBytes = HashCode + .fromString("010000000100010000000000" + "0094357700" + "000000bf" + + "3501030900000006040000000900000029302009000000040000000f1ab4000000330403090000003525010a000000260a000000320903350703090000003526010a0000001b0a000000cd322801331601000000003317010100000033180102000000331901030000003505020a0000001b0a000000a1320b033205041e050000001833000509000000320a033203041ab400000033160105000000331701060000003318010700000033190108000000320304320b033203041ab7" + + "00000048" + + "5e211280259d2f3130248482c2dfc53be2fd5f9bedc9bc21425f951e8097a21900000000c80000003ac8716ad810191acf270d22e9f47f27806256c10d6ba6144900000000000000") + .asBytes(); + BigDecimal amount = BigDecimal.valueOf(500.0).setScale(8); + BigDecimal fee = BigDecimal.valueOf(20.0).setScale(8); + long timestamp = 1439997077932L; + byte[] reference = Base58.decode("2D3jX1pEgu6irsQ7QzJb85QP1D9M45dNyP5M9a3WFHndU5ZywF4F5pnUurcbzMnGMcTwpAY6H7DuLw8cUBU66ao1"); + byte[] signature = Base58.decode("2dZ4megUyNoYYY7qWmuSd4xw1yUKgPPF97yBbeddh8aKuC8PLpz7Xvf3r6Zjv1zwGrR8fEAHuaztCPD4KQp76KdL"); + + DeployATTransactionData transactionData = new DeployATTransactionData(creatorPublicKey, name, description, ATType, tags, creationBytes, amount, fee, + timestamp, reference, signature); + + try (final Repository repository = RepositoryManager.getRepository()) { + repository.getTransactionRepository().save(transactionData); + + DeployATTransaction transaction = new DeployATTransaction(repository, transactionData); + + // Fake entry for this transaction at block height 125598 if it doesn't already exist + if (transaction.getHeight() == 0) { + byte[] blockSignature = Base58.decode( + "2amu634LnAbxeLfDtWdTLiCWtKu1XM2XLK9o6fDM7yGNNoh5Tq2KxSLdx8AS486zUU1wYNGCm8mcGxjMiww979MxdPVB2PQzaKrW2aFn9hpdSNN6Nk7EmeYKwsZdx9tkpHfBt5thSrUUrhzXJju9KYCAP6p3Ty4zccFkaxCP15j332U"); + byte[] generatorSignature = Arrays.copyOfRange(blockSignature, 0, 64); + byte[] transactionsSignature = Arrays.copyOfRange(blockSignature, 64, 128); + + // Check block exists too + if (repository.getBlockRepository().fromSignature(blockSignature) == null) { + int version = 2; + byte[] blockReference = blockSignature; + int transactionCount = 0; + BigDecimal totalFees = BigDecimal.valueOf(70.0).setScale(8); + int height = 125598; + long blockTimestamp = 1439997158336L; + BigDecimal generatingBalance = BigDecimal.valueOf(1440368826L).setScale(8); + byte[] generatorPublicKey = Base58.decode("X4s833bbtghh7gejmaBMbWqD44HrUobw93ANUuaNhFc"); + byte[] atBytes = HashCode.fromString("17950a6c62d17ff0caa545651c054a105f1c464daca443df846cc6a3d58f764b78c09cff50f0fd9ec2").asBytes(); + BigDecimal atFees = BigDecimal.valueOf(50.0).setScale(8); + + BlockData blockData = new BlockData(version, blockReference, transactionCount, totalFees, transactionsSignature, height, blockTimestamp, + generatingBalance, generatorPublicKey, generatorSignature, atBytes, atFees); + + repository.getBlockRepository().save(blockData); + } + + int sequence = 0; + + BlockTransactionData blockTransactionData = new BlockTransactionData(blockSignature, sequence, signature); + repository.getBlockRepository().save(blockTransactionData); + } + + String actualAddress = transaction.getATAccount().getAddress(); + + repository.discardChanges(); + + assertEquals(expectedAddress, actualAddress); + } + } + +} diff --git a/src/test/RepositoryTests.java b/src/test/RepositoryTests.java index e2df05ec..a37b71c6 100644 --- a/src/test/RepositoryTests.java +++ b/src/test/RepositoryTests.java @@ -2,6 +2,8 @@ package test; import static org.junit.Assert.*; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.junit.Test; import repository.DataException; @@ -10,6 +12,8 @@ import repository.RepositoryManager; public class RepositoryTests extends Common { + private static final Logger LOGGER = LogManager.getLogger(RepositoryTests.class); + @Test public void testGetRepository() throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { @@ -45,6 +49,8 @@ public class RepositoryTests extends Common { fail(); } catch (NullPointerException | DataException e) { } + + LOGGER.warn("Expect \"repository already closed\" complaint below"); } } diff --git a/src/test/SaveTests.java b/src/test/SaveTests.java index e57ab170..1338e3b8 100644 --- a/src/test/SaveTests.java +++ b/src/test/SaveTests.java @@ -27,6 +27,8 @@ public class SaveTests extends Common { BigDecimal.ONE, Instant.now().getEpochSecond(), reference, signature); repository.getTransactionRepository().save(paymentTransactionData); + + repository.discardChanges(); } } diff --git a/src/test/TransactionTests.java b/src/test/TransactionTests.java index 9365f91f..f976c59f 100644 --- a/src/test/TransactionTests.java +++ b/src/test/TransactionTests.java @@ -4,6 +4,7 @@ import static org.junit.Assert.*; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -616,7 +617,7 @@ public class TransactionTests { String assetName = "test asset"; String description = "test asset description"; long quantity = 1_000_000L; - boolean isDivisible = false; + boolean isDivisible = true; BigDecimal fee = BigDecimal.ONE; long timestamp = parentBlockData.getTimestamp() + 1_000; @@ -956,16 +957,20 @@ public class TransactionTests { assertNotNull(originalOrderData); assertFalse(originalOrderData.getIsClosed()); + // Unfulfilled order: "buyer" has 10 QORA and wants to buy "test asset" at a price of 50 "test asset" per QORA. + // buyer's order: have=QORA, amount=10, want=test-asset, price=50 (test-asset per QORA, so max return is 500 test-asset) + // Original asset owner (sender) will sell asset to "buyer" // Order: seller has 40 "test asset" and wants to buy QORA at a price of 1/60 QORA per "test asset". // This order should be a partial match for original order, and at a better price than asked - long haveAssetId = Asset.QORA; + long haveAssetId = assetId; BigDecimal amount = BigDecimal.valueOf(40).setScale(8); - long wantAssetId = assetId; - BigDecimal price = BigDecimal.ONE.setScale(8).divide(BigDecimal.valueOf(60).setScale(8)); + long wantAssetId = Asset.QORA; + BigDecimal price = BigDecimal.ONE.setScale(8).divide(BigDecimal.valueOf(60).setScale(8), RoundingMode.DOWN); BigDecimal fee = BigDecimal.ONE; long timestamp = parentBlockData.getTimestamp() + 1_000; + BigDecimal senderPreTradeWantBalance = sender.getConfirmedBalance(wantAssetId); CreateOrderTransactionData createOrderTransactionData = new CreateOrderTransactionData(sender.getPublicKey(), haveAssetId, wantAssetId, amount, price, fee, timestamp, reference); @@ -989,20 +994,19 @@ public class TransactionTests { byte[] orderId = createOrderTransactionData.getSignature(); OrderData orderData = assetRepo.fromOrderId(orderId); assertNotNull(orderData); - assertFalse(orderData.getIsFulfilled()); // Check order has trades List trades = assetRepo.getOrdersTrades(orderId); assertNotNull(trades); - assertEquals(1, trades.size()); + assertEquals("Trade didn't happen", 1, trades.size()); TradeData tradeData = trades.get(0); // Check trade has correct values - BigDecimal expectedAmount = amount.multiply(price); + BigDecimal expectedAmount = amount.divide(originalOrderData.getPrice()).setScale(8); BigDecimal actualAmount = tradeData.getAmount(); assertTrue(expectedAmount.compareTo(actualAmount) == 0); - BigDecimal expectedPrice = originalOrderData.getPrice().multiply(amount); + BigDecimal expectedPrice = amount; BigDecimal actualPrice = tradeData.getPrice(); assertTrue(expectedPrice.compareTo(actualPrice) == 0); @@ -1017,10 +1021,17 @@ public class TransactionTests { assertTrue(expectedBalance.compareTo(actualBalance) == 0); // Check seller's QORA balance - expectedBalance = initialSenderBalance.subtract(BigDecimal.ONE).subtract(BigDecimal.ONE); + expectedBalance = senderPreTradeWantBalance.subtract(BigDecimal.ONE).add(expectedAmount); actualBalance = sender.getConfirmedBalance(wantAssetId); assertTrue(expectedBalance.compareTo(actualBalance) == 0); + // Check seller's order is correctly fulfilled + assertTrue(orderData.getIsFulfilled()); + + // Check buyer's order is still not fulfilled + OrderData buyersOrderData = assetRepo.fromOrderId(originalOrderData.getOrderId()); + assertFalse(buyersOrderData.getIsFulfilled()); + // Orphan transaction block.orphan(); repository.saveChanges(); diff --git a/src/transform/block/BlockTransformer.java b/src/transform/block/BlockTransformer.java index 0542948a..7d7336ae 100644 --- a/src/transform/block/BlockTransformer.java +++ b/src/transform/block/BlockTransformer.java @@ -289,6 +289,24 @@ public class BlockTransformer extends Transformer { } } + public static byte[] getBytesForGeneratorSignature(byte[] generatorSignature, BigDecimal generatingBalance, PublicKeyAccount generator) + throws TransformationException { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH); + + bytes.write(generatorSignature); + + bytes.write(Longs.toByteArray(generatingBalance.longValue())); + + // We're padding here just in case the generator is the genesis account whose public key is only 8 bytes long. + bytes.write(Bytes.ensureCapacity(generator.getPublicKey(), GENERATOR_LENGTH, 0)); + + return bytes.toByteArray(); + } catch (IOException e) { + throw new TransformationException(e); + } + } + public static byte[] getBytesForTransactionsSignature(Block block) throws TransformationException { ByteArrayOutputStream bytes = new ByteArrayOutputStream( GENERATOR_SIGNATURE_LENGTH + block.getBlockData().getTransactionCount() * TransactionTransformer.SIGNATURE_LENGTH); diff --git a/src/transform/transaction/DeployATTransactionTransformer.java b/src/transform/transaction/DeployATTransactionTransformer.java new file mode 100644 index 00000000..0699084f --- /dev/null +++ b/src/transform/transaction/DeployATTransactionTransformer.java @@ -0,0 +1,188 @@ +package transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; + +import org.json.simple.JSONObject; + +import com.google.common.base.Utf8; +import com.google.common.hash.HashCode; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +import data.transaction.TransactionData; +import qora.account.PublicKeyAccount; +import qora.block.BlockChain; +import qora.transaction.DeployATTransaction; +import data.transaction.DeployATTransactionData; +import transform.TransformationException; +import utils.Serialization; + +public class DeployATTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int CREATOR_LENGTH = PUBLIC_KEY_LENGTH; + private static final int NAME_SIZE_LENGTH = INT_LENGTH; + private static final int DESCRIPTION_SIZE_LENGTH = INT_LENGTH; + private static final int AT_TYPE_SIZE_LENGTH = INT_LENGTH; + 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 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; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + long timestamp = byteBuffer.getLong(); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] creatorPublicKey = Serialization.deserializePublicKey(byteBuffer); + + String name = Serialization.deserializeSizedString(byteBuffer, DeployATTransaction.MAX_NAME_SIZE); + + String description = Serialization.deserializeSizedString(byteBuffer, DeployATTransaction.MAX_DESCRIPTION_SIZE); + + String ATType = Serialization.deserializeSizedString(byteBuffer, DeployATTransaction.MAX_AT_TYPE_SIZE); + + String tags = Serialization.deserializeSizedString(byteBuffer, DeployATTransaction.MAX_TAGS_SIZE); + + int creationBytesSize = byteBuffer.getInt(); + if (creationBytesSize <= 0 || creationBytesSize > DeployATTransaction.MAX_CREATION_BYTES_SIZE) + throw new TransformationException("Creation bytes size invalid in DeployAT transaction"); + + byte[] creationBytes = new byte[creationBytesSize]; + byteBuffer.get(creationBytes); + + BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); + + 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); + } + + 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; + + return dataLength; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(deployATTransactionData.getType().value)); + bytes.write(Longs.toByteArray(deployATTransactionData.getTimestamp())); + bytes.write(deployATTransactionData.getReference()); + + bytes.write(deployATTransactionData.getCreatorPublicKey()); + + Serialization.serializeSizedString(bytes, deployATTransactionData.getName()); + + Serialization.serializeSizedString(bytes, deployATTransactionData.getDescription()); + + Serialization.serializeSizedString(bytes, deployATTransactionData.getATType()); + + Serialization.serializeSizedString(bytes, deployATTransactionData.getTags()); + + bytes.write(deployATTransactionData.getCreationBytes()); + + Serialization.serializeBigDecimal(bytes, deployATTransactionData.getAmount()); + + Serialization.serializeBigDecimal(bytes, deployATTransactionData.getFee()); + + if (deployATTransactionData.getSignature() != null) + bytes.write(deployATTransactionData.getSignature()); + + return bytes.toByteArray(); + } catch (IOException | ClassCastException e) { + throw new TransformationException(e); + } + } + + /** + * 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()) + return TransactionTransformer.toBytesForSigningImpl(transactionData); + + // Special v1 version + + // Easier to start from scratch + try { + DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(deployATTransactionData.getType().value)); + bytes.write(Longs.toByteArray(deployATTransactionData.getTimestamp())); + bytes.write(deployATTransactionData.getReference()); + + bytes.write(deployATTransactionData.getCreatorPublicKey()); + + Serialization.serializeSizedString(bytes, deployATTransactionData.getName()); + + Serialization.serializeSizedString(bytes, deployATTransactionData.getDescription()); + + // Omitted: Serialization.serializeSizedString(bytes, deployATTransactionData.getATType()); + + // Omitted: Serialization.serializeSizedString(bytes, deployATTransactionData.getTags()); + + bytes.write(deployATTransactionData.getCreationBytes()); + + Serialization.serializeBigDecimal(bytes, deployATTransactionData.getAmount()); + + Serialization.serializeBigDecimal(bytes, deployATTransactionData.getFee()); + + if (deployATTransactionData.getSignature() != null) + bytes.write(deployATTransactionData.getSignature()); + + return bytes.toByteArray(); + } catch (IOException | ClassCastException e) { + throw new TransformationException(e); + } + } + + @SuppressWarnings("unchecked") + public static JSONObject toJSON(TransactionData transactionData) throws TransformationException { + JSONObject json = TransactionTransformer.getBaseJSON(transactionData); + + try { + DeployATTransactionData deployATTransactionData = (DeployATTransactionData) transactionData; + + byte[] creatorPublicKey = deployATTransactionData.getCreatorPublicKey(); + + json.put("creator", PublicKeyAccount.getAddress(creatorPublicKey)); + json.put("creatorPublicKey", HashCode.fromBytes(creatorPublicKey).toString()); + json.put("name", deployATTransactionData.getName()); + json.put("description", deployATTransactionData.getDescription()); + json.put("atType", deployATTransactionData.getATType()); + json.put("tags", deployATTransactionData.getTags()); + json.put("creationBytes", HashCode.fromBytes(deployATTransactionData.getCreationBytes()).toString()); + json.put("amount", deployATTransactionData.getAmount().toPlainString()); + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/transform/transaction/IssueAssetTransactionTransformer.java b/src/transform/transaction/IssueAssetTransactionTransformer.java index 15b873e6..a4ec1b5d 100644 --- a/src/transform/transaction/IssueAssetTransactionTransformer.java +++ b/src/transform/transaction/IssueAssetTransactionTransformer.java @@ -99,9 +99,14 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { bytes.write(Longs.toByteArray(issueAssetTransactionData.getQuantity())); bytes.write((byte) (issueAssetTransactionData.getIsDivisible() ? 1 : 0)); - // In v1, IssueAssetTransaction uses Asset.toBytes which also serializes reference. - if (transactionData.getTimestamp() < BlockChain.getIssueAssetV2Timestamp()) - bytes.write(issueAssetTransactionData.getSignature()); + // In v1, IssueAssetTransaction uses Asset.toBytes which also serializes Asset's reference which is the IssueAssetTransaction's signature + if (transactionData.getTimestamp() < BlockChain.getIssueAssetV2Timestamp()) { + byte[] assetReference = issueAssetTransactionData.getSignature(); + if (assetReference != null) + bytes.write(assetReference); + else + bytes.write(new byte[ASSET_REFERENCE_LENGTH]); + } Serialization.serializeBigDecimal(bytes, issueAssetTransactionData.getFee()); diff --git a/src/transform/transaction/TransactionTransformer.java b/src/transform/transaction/TransactionTransformer.java index 3732f1ee..232ce38f 100644 --- a/src/transform/transaction/TransactionTransformer.java +++ b/src/transform/transaction/TransactionTransformer.java @@ -92,6 +92,9 @@ public class TransactionTransformer extends Transformer { case MESSAGE: return MessageTransactionTransformer.fromByteBuffer(byteBuffer); + case DEPLOY_AT: + return DeployATTransactionTransformer.fromByteBuffer(byteBuffer); + default: throw new TransformationException("Unsupported transaction type [" + type.value + "] during conversion from bytes"); } @@ -150,6 +153,9 @@ public class TransactionTransformer extends Transformer { case MESSAGE: return MessageTransactionTransformer.getDataLength(transactionData); + case DEPLOY_AT: + return DeployATTransactionTransformer.getDataLength(transactionData); + default: throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] when requesting byte length"); } @@ -205,6 +211,9 @@ public class TransactionTransformer extends Transformer { case MESSAGE: return MessageTransactionTransformer.toBytes(transactionData); + case DEPLOY_AT: + return DeployATTransactionTransformer.toBytes(transactionData); + default: throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] during conversion to bytes"); } @@ -269,6 +278,9 @@ public class TransactionTransformer extends Transformer { case MESSAGE: return MessageTransactionTransformer.toBytesForSigningImpl(transactionData); + case DEPLOY_AT: + return DeployATTransactionTransformer.toBytesForSigningImpl(transactionData); + default: throw new TransformationException( "Unsupported transaction type [" + transactionData.getType().value + "] during conversion to bytes for signing"); @@ -345,6 +357,9 @@ public class TransactionTransformer extends Transformer { case MESSAGE: return MessageTransactionTransformer.toJSON(transactionData); + case DEPLOY_AT: + return DeployATTransactionTransformer.toJSON(transactionData); + default: throw new TransformationException("Unsupported transaction type [" + transactionData.getType().value + "] during conversion to JSON"); } diff --git a/src/utils/Serialization.java b/src/utils/Serialization.java index 959c42af..c906ff87 100644 --- a/src/utils/Serialization.java +++ b/src/utils/Serialization.java @@ -17,6 +17,32 @@ public class Serialization { /** * Convert BigDecimal, unscaled, to byte[] then prepend with zero bytes to specified length. * + * @param amount + * @param length + * @return byte[] + * @throws IOException + */ + public static byte[] serializeBigDecimal(BigDecimal amount, int length) throws IOException { + byte[] amountBytes = amount.unscaledValue().toByteArray(); + byte[] output = new byte[length]; + System.arraycopy(amountBytes, 0, output, length - amountBytes.length, amountBytes.length); + return output; + } + + /** + * Convert BigDecimal, unscaled, to byte[] then prepend with zero bytes to fixed length of 8. + * + * @param amount + * @return byte[] + * @throws IOException + */ + public static byte[] serializeBigDecimal(BigDecimal amount) throws IOException { + return serializeBigDecimal(amount, 8); + } + + /** + * Write to ByteBuffer a BigDecimal, unscaled, prepended with zero bytes to specified length. + * * @param ByteArrayOutputStream * @param amount * @param length @@ -30,7 +56,7 @@ public class Serialization { } /** - * Convert BigDecimal, unscaled, to byte[] then prepend with zero bytes to fixed length of 8. + * Write to ByteBuffer a BigDecimal, unscaled, prepended with zero bytes to fixed length of 8. * * @param ByteArrayOutputStream * @param amount @@ -73,9 +99,6 @@ public class Serialization { } public static String deserializeSizedString(ByteBuffer byteBuffer, int maxSize) throws TransformationException { - if (byteBuffer.remaining() < Transformer.INT_LENGTH) - throw new TransformationException("Byte data too short for serialized string size"); - int size = byteBuffer.getInt(); if (size > maxSize) throw new TransformationException("Serialized string too long");