MAJOR: Don't delete transactions when orphaning - make them unconfirmed again

Lots of edits to Transaction subclasses to change/remove 'delete'.

Corresponding extra changes to help reset some transaction fields to pre-process
state during orphaning.

Changed Block, GenesisBlock & Synchronizer to save transactions where appropriate.

Added enhanced GET_SIGNATURES_V2 network message to reduce the number of
block signatures sent over network.

Peers are now version 2 if they send a new-style build version string,
instead of using first digit from build version.
This commit is contained in:
catbref
2019-05-22 14:50:37 +01:00
parent 07e8e01865
commit a3d4cf2900
56 changed files with 410 additions and 444 deletions

View File

@@ -3,23 +3,37 @@ package org.qora.test;
import java.math.BigDecimal;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.qora.account.PrivateKeyAccount;
import org.qora.block.Block;
import org.qora.block.BlockGenerator;
import org.qora.block.GenesisBlock;
import org.qora.data.at.ATStateData;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.test.common.Common;
import org.qora.test.common.TransactionUtils;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.transform.TransformationException;
import org.qora.transform.block.BlockTransformer;
import org.qora.transform.transaction.TransactionTransformer;
import org.qora.utils.Base58;
import org.qora.utils.Triple;
import static org.junit.Assert.*;
public class BlockTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testGenesisBlockTransactions() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
@@ -33,20 +47,24 @@ public class BlockTests extends Common {
List<Transaction> transactions = block.getTransactions();
assertNotNull(transactions);
byte[] lastGenesisSignature = null;
for (Transaction transaction : transactions) {
assertNotNull(transaction);
TransactionData transactionData = transaction.getTransactionData();
assertEquals(Transaction.TransactionType.GENESIS, transactionData.getType());
if (transactionData.getType() != Transaction.TransactionType.GENESIS)
continue;
assertTrue(transactionData.getFee().compareTo(BigDecimal.ZERO) == 0);
assertNull(transactionData.getReference());
assertTrue(transaction.isSignatureValid());
assertEquals(Transaction.ValidationResult.OK, transaction.isValid());
lastGenesisSignature = transactionData.getSignature();
}
// Attempt to load first transaction directly from database
TransactionData transactionData = repository.getTransactionRepository().fromSignature(transactions.get(0).getTransactionData().getSignature());
// Attempt to load last GENESIS transaction directly from database
TransactionData transactionData = repository.getTransactionRepository().fromSignature(lastGenesisSignature);
assertNotNull(transactionData);
assertEquals(Transaction.TransactionType.GENESIS, transactionData.getType());
@@ -61,61 +79,59 @@ public class BlockTests extends Common {
}
}
@Test
public void testBlockPaymentTransactions() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
// Block 949 has lots of varied transactions
// Blocks 390 & 754 have only payment transactions
BlockData blockData = repository.getBlockRepository().fromHeight(754);
assertNotNull("Block 754 is required for this test", blockData);
Block block = new Block(repository, blockData);
assertTrue(block.isSignatureValid());
List<Transaction> transactions = block.getTransactions();
assertNotNull(transactions);
for (Transaction transaction : transactions) {
assertNotNull(transaction);
TransactionData transactionData = transaction.getTransactionData();
assertEquals(Transaction.TransactionType.PAYMENT, transactionData.getType());
assertFalse(transactionData.getFee().compareTo(BigDecimal.ZERO) == 0);
assertNotNull(transactionData.getReference());
assertTrue(transaction.isSignatureValid());
}
// Attempt to load first transaction directly from database
TransactionData transactionData = repository.getTransactionRepository().fromSignature(transactions.get(0).getTransactionData().getSignature());
assertNotNull(transactionData);
assertEquals(Transaction.TransactionType.PAYMENT, transactionData.getType());
assertFalse(transactionData.getFee().compareTo(BigDecimal.ZERO) == 0);
assertNotNull(transactionData.getReference());
Transaction transaction = Transaction.fromData(repository, transactionData);
assertNotNull(transaction);
assertTrue(transaction.isSignatureValid());
}
}
@Test
public void testBlockSerialization() throws DataException, TransformationException {
try (final Repository repository = RepositoryManager.getRepository()) {
// Block 949 has lots of varied transactions
// Blocks 390 & 754 have only payment transactions
BlockData blockData = repository.getBlockRepository().fromHeight(754);
assertNotNull("Block 754 is required for this test", blockData);
PrivateKeyAccount signingAccount = Common.getTestAccount(repository, "alice");
// TODO: Fill block with random, valid transactions of every type (except GENESIS or AT)
for (Transaction.TransactionType txType : Transaction.TransactionType.values()) {
if (txType == TransactionType.GENESIS || txType == TransactionType.AT)
continue;
TransactionData transactionData = TransactionUtils.randomTransaction(repository, signingAccount, txType, true);
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(signingAccount);
repository.getTransactionRepository().save(transactionData);
repository.getTransactionRepository().unconfirmTransaction(transactionData);
repository.saveChanges();
// TODO: more transactions
break;
}
// We might need to wait until transactions' timestamps are valid for the block we're about to generate
try {
Thread.sleep(1L);
} catch (InterruptedException e) {
}
BlockGenerator.generateTestingBlock(repository, signingAccount);
BlockData blockData = repository.getBlockRepository().getLastBlock();
Block block = new Block(repository, blockData);
assertTrue(block.isSignatureValid());
byte[] bytes = BlockTransformer.toBytes(block);
assertEquals(BlockTransformer.getDataLength(block), bytes.length);
Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo = BlockTransformer.fromBytes(bytes);
// Compare transactions
List<TransactionData> deserializedTransactions = blockInfo.getB();
assertEquals("Transaction count differs", blockData.getTransactionCount(), deserializedTransactions.size());
for (int i = 0; i < blockData.getTransactionCount(); ++i) {
TransactionData deserializedTransactionData = deserializedTransactions.get(i);
Transaction originalTransaction = block.getTransactions().get(i);
TransactionData originalTransactionData = originalTransaction.getTransactionData();
assertEquals("Transaction signature differs", Base58.encode(originalTransactionData.getSignature()), Base58.encode(deserializedTransactionData.getSignature()));
assertEquals("Transaction declared length differs", TransactionTransformer.getDataLength(originalTransactionData), TransactionTransformer.getDataLength(deserializedTransactionData));
assertEquals("Transaction serialized length differs", TransactionTransformer.toBytes(originalTransactionData).length, TransactionTransformer.toBytes(deserializedTransactionData).length);
}
}
}

View File

@@ -1,15 +0,0 @@
package org.qora.test;
import org.junit.Test;
import org.qora.block.BlockChain;
import org.qora.repository.DataException;
import org.qora.test.common.Common;
public class BlockchainTests extends Common {
@Test
public void testValidateOrRebuild() throws DataException {
BlockChain.validate();
}
}

View File

@@ -1,54 +0,0 @@
package org.qora.test;
import java.math.BigDecimal;
import java.util.List;
import org.junit.Test;
import org.qora.block.Block;
import org.qora.block.GenesisBlock;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.test.common.Common;
import org.qora.transaction.Transaction;
import static org.junit.Assert.*;
public class GenesisTests extends Common {
@Test
public void testGenesisBlockTransactions() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight());
GenesisBlock block = GenesisBlock.getInstance(repository);
assertNotNull("No genesis block?", block);
assertTrue(block.isSignatureValid());
// Note: only true if blockchain is empty
assertEquals("Block invalid", Block.ValidationResult.OK, block.isValid());
List<Transaction> transactions = block.getTransactions();
assertNotNull("No transactions?", transactions);
for (Transaction transaction : transactions) {
assertNotNull(transaction);
TransactionData transactionData = transaction.getTransactionData();
assertEquals(Transaction.TransactionType.GENESIS, transactionData.getType());
assertTrue(transactionData.getFee().compareTo(BigDecimal.ZERO) == 0);
assertNull(transactionData.getReference());
assertNotNull(transactionData.getSignature());
assertTrue(transaction.isSignatureValid());
assertEquals(Transaction.ValidationResult.OK, transaction.isValid());
}
// Actually try to process genesis block onto empty blockchain
block.process();
repository.saveChanges();
}
}
}

View File

@@ -1,93 +0,0 @@
package org.qora.test;
import org.junit.Test;
import org.qora.account.PublicKeyAccount;
import org.qora.data.transaction.PaymentTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.repository.TransactionRepository;
import org.qora.test.common.Common;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.utils.Base58;
import static org.junit.Assert.*;
public class LoadTests extends Common {
@Test
public void testLoadPaymentTransaction() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionRepository transactionRepository = repository.getTransactionRepository();
assertTrue("Migrate from old database to at least block 49778 before running this test", repository.getBlockRepository().getBlockchainHeight() >= 49778);
String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt";
byte[] signature = Base58.decode(signature58);
TransactionData transactionData = transactionRepository.fromSignature(signature);
assertNotNull("Transaction data not loaded from repository", transactionData);
assertEquals("Transaction data not PAYMENT type", TransactionType.PAYMENT, transactionData.getType());
assertEquals("QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E", PublicKeyAccount.getAddress(transactionData.getCreatorPublicKey()));
PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData;
assertNotNull(paymentTransactionData);
assertEquals("QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E", PublicKeyAccount.getAddress(paymentTransactionData.getSenderPublicKey()));
assertEquals("QZsv8vbJ6QfrBNba4LMp5UtHhAzhrxvVUU", paymentTransactionData.getRecipient());
assertEquals(1416209264000L, paymentTransactionData.getTimestamp());
assertEquals("31dC6kHHBeG5vYb8LMaZDjLEmhc9kQB2VUApVd8xWncSRiXu7yMejdprjYFMP2rUnzZxWd4KJhkq6LsV7rQvU1kY",
Base58.encode(paymentTransactionData.getReference()));
}
}
@Test
public void testLoadFactory() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionRepository transactionRepository = repository.getTransactionRepository();
assertTrue("Migrate from old database to at least block 49778 before running this test", repository.getBlockRepository().getBlockchainHeight() >= 49778);
String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt";
byte[] signature = Base58.decode(signature58);
while (true) {
TransactionData transactionData = transactionRepository.fromSignature(signature);
if (transactionData == null)
break;
if (transactionData.getType() != TransactionType.PAYMENT)
break;
PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData;
System.out.println(PublicKeyAccount.getAddress(paymentTransactionData.getSenderPublicKey()) + " sent " + paymentTransactionData.getAmount()
+ " QORA to " + paymentTransactionData.getRecipient());
signature = transactionData.getReference();
}
}
}
@Test
public void testLoadNonexistentTransaction() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionRepository transactionRepository = repository.getTransactionRepository();
String signature58 = "1111222233334444";
byte[] signature = Base58.decode(signature58);
TransactionData transactionData = transactionRepository.fromSignature(signature);
if (transactionData != null) {
PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData;
System.out.println(PublicKeyAccount.getAddress(paymentTransactionData.getSenderPublicKey()) + " sent " + paymentTransactionData.getAmount()
+ " QORA to " + paymentTransactionData.getRecipient());
fail();
}
}
}
}

View File

@@ -1,46 +0,0 @@
package org.qora.test;
import org.junit.Test;
import org.qora.data.block.BlockData;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.repository.RepositoryManager;
import org.qora.repository.TransactionRepository;
import org.qora.test.common.Common;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.utils.Base58;
import static org.junit.Assert.*;
public class NavigationTests extends Common {
@Test
public void testNavigateFromTransactionToBlock() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TransactionRepository transactionRepository = repository.getTransactionRepository();
assertTrue("Migrate from old database to at least block 49778 before running this test", repository.getBlockRepository().getBlockchainHeight() >= 49778);
String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt";
byte[] signature = Base58.decode(signature58);
System.out.println("Navigating to Block from transaction " + signature58);
TransactionData transactionData = transactionRepository.fromSignature(signature);
assertNotNull("Transaction data not loaded from repository", transactionData);
assertEquals("Transaction data not PAYMENT type", TransactionType.PAYMENT, transactionData.getType());
int transactionHeight = transactionRepository.getHeightFromSignature(signature);
assertFalse("Transaction not found or transaction's block not found", transactionHeight == 0);
assertEquals("Transaction's block height expected to be 49778", 49778, transactionHeight);
BlockData blockData = repository.getBlockRepository().fromHeight(transactionHeight);
assertNotNull("Block 49778 not loaded from database", blockData);
System.out.println("Block " + blockData.getHeight() + ", signature: " + Base58.encode(blockData.getSignature()));
assertEquals((Integer) 49778, blockData.getHeight());
}
}
}

View File

@@ -1,5 +1,6 @@
package org.qora.test;
import org.junit.Before;
import org.junit.Test;
import org.qora.account.Account;
import org.qora.asset.Asset;
@@ -19,6 +20,11 @@ public class RepositoryTests extends Common {
private static final Logger LOGGER = LogManager.getLogger(RepositoryTests.class);
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testGetRepository() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
@@ -44,7 +50,7 @@ public class RepositoryTests extends Common {
@Test
public void testAccessAfterClose() throws DataException {
try (Repository repository = RepositoryManager.getRepository()) {
try (final Repository repository = RepositoryManager.getRepository()) {
assertNotNull(repository);
repository.close();
@@ -61,17 +67,15 @@ public class RepositoryTests extends Common {
@Test
public void testDeadlock() throws DataException {
Common.useDefaultSettings();
// Open connection 1
try (Repository repository1 = RepositoryManager.getRepository()) {
try (final Repository repository1 = RepositoryManager.getRepository()) {
// Do a database 'read'
Account account1 = Common.getTestAccount(repository1, "alice");
account1.getLastReference();
// Open connection 2
try (Repository repository2 = RepositoryManager.getRepository()) {
try (final Repository repository2 = RepositoryManager.getRepository()) {
// Update account in 2
Account account2 = Common.getTestAccount(repository2, "alice");
account2.setConfirmedBalance(Asset.QORA, BigDecimal.valueOf(1234L));

View File

@@ -3,17 +3,21 @@ package org.qora.test.common;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.qora.account.PrivateKeyAccount;
import org.qora.block.BlockGenerator;
import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException;
import org.qora.repository.Repository;
import org.qora.transaction.Transaction;
import org.qora.transaction.Transaction.TransactionType;
import org.qora.transaction.Transaction.ValidationResult;
public class TransactionUtils {
public static void signAndForge(Repository repository, TransactionData transactionData, PrivateKeyAccount signingAccount) throws DataException {
public static void signAsUnconfirmed(Repository repository, TransactionData transactionData, PrivateKeyAccount signingAccount) throws DataException {
Transaction transaction = Transaction.fromData(repository, transactionData);
transaction.sign(signingAccount);
@@ -32,10 +36,29 @@ public class TransactionUtils {
repository.getTransactionRepository().save(transactionData);
repository.getTransactionRepository().unconfirmTransaction(transactionData);
repository.saveChanges();
}
public static void signAndForge(Repository repository, TransactionData transactionData, PrivateKeyAccount signingAccount) throws DataException {
signAsUnconfirmed(repository, transactionData, signingAccount);
// Generate block
PrivateKeyAccount generatorAccount = Common.getTestAccount(repository, "alice");
BlockGenerator.generateTestingBlock(repository, generatorAccount);
}
public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, TransactionType txType, boolean wantValid) throws DataException {
try {
Class <?> clazz = Class.forName(String.join("", org.qora.test.common.transaction.Transaction.class.getPackage().getName(), ".", txType.className, "Transaction"));
try {
Method method = clazz.getDeclaredMethod("randomTransaction", Repository.class, PrivateKeyAccount.class, boolean.class);
return (TransactionData) method.invoke(null, repository, account, wantValid);
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException(String.format("Transaction subclass constructor not found for transaction type \"%s\"", txType.name()), e);
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(String.format("Transaction subclass not found for transaction type \"%s\"", txType.name()), e);
}
}
}

View File

@@ -0,0 +1,18 @@
package org.qora.test.common.transaction;
import java.math.BigDecimal;
import org.qora.account.PrivateKeyAccount;
import org.qora.data.transaction.PaymentTransactionData;
import org.qora.data.transaction.TransactionData;
import org.qora.group.Group;
import org.qora.repository.Repository;
import org.qora.utils.NTP;
public class PaymentTransaction {
public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, boolean wantValid) {
return new PaymentTransactionData(NTP.getTime(), Group.NO_GROUP, new byte[32], account.getPublicKey(), account.getAddress(), BigDecimal.valueOf(123L), BigDecimal.ONE);
}
}

View File

@@ -0,0 +1,4 @@
package org.qora.test.common.transaction;
public abstract class Transaction {
}

View File

@@ -22,7 +22,6 @@ import org.qora.transaction.Transaction.ValidationResult;
public class GrantForgingTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();