Merge branch 'master' into AT-sleep-until-message

This commit is contained in:
CalDescent
2021-08-01 10:30:52 +01:00
217 changed files with 22030 additions and 9523 deletions

View File

@@ -7,6 +7,7 @@ import java.util.List;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.block.Block;
@@ -83,6 +84,7 @@ public class BlockTests extends Common {
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testBlockSerialization() throws DataException, TransformationException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount signingAccount = Common.getTestAccount(repository, "alice");

View File

@@ -3,10 +3,12 @@ package org.qortal.test;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import org.junit.Before;
import org.junit.Test;
@@ -28,15 +30,13 @@ public class ByteArrayTests {
}
}
private void fillMap(Map<ByteArray, String> map) {
private static void fillMap(Map<ByteArray, String> map) {
for (byte[] testValue : testValues)
map.put(new ByteArray(testValue), String.valueOf(map.size()));
}
private byte[] dup(byte[] value) {
byte[] copiedValue = new byte[value.length];
System.arraycopy(value, 0, copiedValue, 0, copiedValue.length);
return copiedValue;
private static byte[] dup(byte[] value) {
return Arrays.copyOf(value, value.length);
}
@Test
@@ -92,7 +92,7 @@ public class ByteArrayTests {
@Test
@SuppressWarnings("unlikely-arg-type")
public void testMapContainsKey() {
public void testHashMapContainsKey() {
Map<ByteArray, String> testMap = new HashMap<>();
fillMap(testMap);
@@ -105,8 +105,59 @@ public class ByteArrayTests {
assertTrue("boxed not equal to primitive", ba.equals(copiedValue));
// This won't work because copiedValue.hashCode() will not match ba.hashCode()
assertFalse("Primitive shouldn't be found in map", testMap.containsKey(copiedValue));
/*
* Unfortunately this doesn't work because HashMap::containsKey compares hashCodes first,
* followed by object references, and copiedValue.hashCode() will never match ba.hashCode().
*/
assertFalse("Primitive shouldn't be found in HashMap", testMap.containsKey(copiedValue));
}
@Test
@SuppressWarnings("unlikely-arg-type")
public void testTreeMapContainsKey() {
Map<ByteArray, String> testMap = new TreeMap<>();
fillMap(testMap);
// Create new ByteArray object with an existing value.
byte[] copiedValue = dup(testValues.get(3));
ByteArray ba = new ByteArray(copiedValue);
// Confirm object can be found in map
assertTrue("ByteArray not found in map", testMap.containsKey(ba));
assertTrue("boxed not equal to primitive", ba.equals(copiedValue));
/*
* Unfortunately this doesn't work because TreeMap::containsKey(x) wants to cast x to
* Comparable<? super ByteArray> and byte[] does not fit <? super ByteArray>
* so this throws a ClassCastException.
*/
try {
assertFalse("Primitive shouldn't be found in TreeMap", testMap.containsKey(copiedValue));
fail();
} catch (ClassCastException e) {
// Expected
}
}
@Test
@SuppressWarnings("unlikely-arg-type")
public void testArrayListContains() {
// Create new ByteArray object with an existing value.
byte[] copiedValue = dup(testValues.get(3));
ByteArray ba = new ByteArray(copiedValue);
// Confirm object can be found in list
assertTrue("ByteArray not found in map", testValues.contains(ba));
assertTrue("boxed not equal to primitive", ba.equals(copiedValue));
/*
* Unfortunately this doesn't work because ArrayList::contains performs
* copiedValue.equals(x) for each x in testValues, and byte[].equals()
* simply compares object references, so will never match any ByteArray.
*/
assertFalse("Primitive shouldn't be found in ArrayList", testValues.contains(copiedValue));
}
@Test
@@ -116,8 +167,9 @@ public class ByteArrayTests {
byte[] copiedValue = dup(testValue);
System.out.println(String.format("Primitive hashCode: 0x%08x", testValue.hashCode()));
System.out.println(String.format("Boxed hashCode: 0x%08x", ba1.hashCode()));
System.out.println(String.format("Primitive hashCode: 0x%08x", copiedValue.hashCode()));
System.out.println(String.format("Duplicated primitive hashCode: 0x%08x", copiedValue.hashCode()));
}
@Test

View File

@@ -3,12 +3,15 @@ package org.qortal.test;
import static org.junit.Assert.*;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.qortal.account.Account;
import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.data.block.BlockSummaryData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -17,12 +20,21 @@ import org.qortal.test.common.Common;
import org.qortal.test.common.TestAccount;
import org.qortal.transform.Transformer;
import org.qortal.transform.block.BlockTransformer;
import org.qortal.utils.NTP;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
public class ChainWeightTests extends Common {
private static final Random RANDOM = new Random();
private static final NumberFormat FORMATTER = new DecimalFormat("0.###E0");
@BeforeClass
public static void beforeClass() {
// We need this so that NTP.getTime() in Block.calcChainWeight() doesn't return null, causing NPE
NTP.setFixedOffset(0L);
}
@Before
public void beforeTest() throws DataException {
@@ -89,7 +101,97 @@ public class ChainWeightTests extends Common {
}
}
// Check that a longer chain beats a shorter chain
// Demonstrates that typical key distance ranges from roughly 1E75 to 1E77
@Test
public void testKeyDistances() {
byte[] parentMinterKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
byte[] testKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
for (int i = 0; i < 50; ++i) {
int parentHeight = RANDOM.nextInt(50000);
RANDOM.nextBytes(parentMinterKey);
RANDOM.nextBytes(testKey);
int minterLevel = RANDOM.nextInt(10) + 1;
BigInteger keyDistance = Block.calcKeyDistance(parentHeight, parentMinterKey, testKey, minterLevel);
System.out.println(String.format("Parent height: %d, minter level: %d, distance: %s",
parentHeight,
minterLevel,
FORMATTER.format(keyDistance)));
}
}
// If typical key distance ranges from 1E75 to 1E77
// then we want lots of online accounts to push a 1E75 distance
// towards 1E77 so that it competes with a 1E77 key that has hardly any online accounts
// 1E75 is approx. 2**249 so maybe that's a good value for Block.ACCOUNTS_COUNT_SHIFT
@Test
public void testMoreAccountsVersusKeyDistance() throws DataException {
BigInteger minimumBetterKeyDistance = BigInteger.TEN.pow(77);
BigInteger maximumWorseKeyDistance = BigInteger.TEN.pow(75);
try (final Repository repository = RepositoryManager.getRepository()) {
final byte[] parentMinterKey = new byte[Transformer.PUBLIC_KEY_LENGTH];
TestAccount betterAccount = Common.getTestAccount(repository, "bob-reward-share");
byte[] betterKey = betterAccount.getPublicKey();
int betterMinterLevel = Account.getRewardShareEffectiveMintingLevel(repository, betterKey);
TestAccount worseAccount = Common.getTestAccount(repository, "dilbert-reward-share");
byte[] worseKey = worseAccount.getPublicKey();
int worseMinterLevel = Account.getRewardShareEffectiveMintingLevel(repository, worseKey);
// This is to check that the hard-coded keys ARE actually better/worse as expected, before moving on testing more online accounts
BigInteger betterKeyDistance;
BigInteger worseKeyDistance;
int parentHeight = 0;
do {
++parentHeight;
betterKeyDistance = Block.calcKeyDistance(parentHeight, parentMinterKey, betterKey, betterMinterLevel);
worseKeyDistance = Block.calcKeyDistance(parentHeight, parentMinterKey, worseKey, worseMinterLevel);
} while (betterKeyDistance.compareTo(minimumBetterKeyDistance) < 0 || worseKeyDistance.compareTo(maximumWorseKeyDistance) > 0);
System.out.println(String.format("Parent height: %d, better key distance: %s, worse key distance: %s",
parentHeight,
FORMATTER.format(betterKeyDistance),
FORMATTER.format(worseKeyDistance)));
for (int accountsCountShift = 244; accountsCountShift <= 256; accountsCountShift += 2) {
for (int worseAccountsCount = 1; worseAccountsCount <= 101; worseAccountsCount += 25) {
for (int betterAccountsCount = 1; betterAccountsCount <= 1001; betterAccountsCount += 250) {
BlockSummaryData worseKeyBlockSummary = new BlockSummaryData(parentHeight + 1, null, worseKey, betterAccountsCount);
BlockSummaryData betterKeyBlockSummary = new BlockSummaryData(parentHeight + 1, null, betterKey, worseAccountsCount);
populateBlockSummaryMinterLevel(repository, worseKeyBlockSummary);
populateBlockSummaryMinterLevel(repository, betterKeyBlockSummary);
BigInteger worseKeyBlockWeight = calcBlockWeight(parentHeight, parentMinterKey, worseKeyBlockSummary, accountsCountShift);
BigInteger betterKeyBlockWeight = calcBlockWeight(parentHeight, parentMinterKey, betterKeyBlockSummary, accountsCountShift);
System.out.println(String.format("Shift: %d, worse key: %d accounts, %s diff; better key: %d accounts: %s diff; winner: %s",
accountsCountShift,
betterAccountsCount, // used with worseKey
FORMATTER.format(worseKeyBlockWeight),
worseAccountsCount, // used with betterKey
FORMATTER.format(betterKeyBlockWeight),
worseKeyBlockWeight.compareTo(betterKeyBlockWeight) > 0 ? "worse key/better accounts" : "better key/worse accounts"
));
}
}
System.out.println();
}
}
}
private static BigInteger calcBlockWeight(int parentHeight, byte[] parentBlockSignature, BlockSummaryData blockSummaryData, int accountsCountShift) {
BigInteger keyDistance = Block.calcKeyDistance(parentHeight, parentBlockSignature, blockSummaryData.getMinterPublicKey(), blockSummaryData.getMinterLevel());
return BigInteger.valueOf(blockSummaryData.getOnlineAccountsCount()).shiftLeft(accountsCountShift).add(keyDistance);
}
// Check that a longer chain has same weight as shorter/truncated chain
@Test
public void testLongerChain() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
@@ -97,18 +199,20 @@ public class ChainWeightTests extends Common {
BlockSummaryData commonBlockSummary = genBlockSummary(repository, commonBlockHeight);
byte[] commonBlockGeneratorKey = commonBlockSummary.getMinterPublicKey();
List<BlockSummaryData> shorterChain = genBlockSummaries(repository, 3, commonBlockSummary);
List<BlockSummaryData> longerChain = genBlockSummaries(repository, shorterChain.size() + 1, commonBlockSummary);
populateBlockSummariesMinterLevels(repository, shorterChain);
List<BlockSummaryData> longerChain = genBlockSummaries(repository, 6, commonBlockSummary);
populateBlockSummariesMinterLevels(repository, longerChain);
List<BlockSummaryData> shorterChain = longerChain.subList(0, longerChain.size() / 2);
final int mutualHeight = commonBlockHeight - 1 + Math.min(shorterChain.size(), longerChain.size());
BigInteger shorterChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, shorterChain, mutualHeight);
BigInteger longerChainWeight = Block.calcChainWeight(commonBlockHeight, commonBlockGeneratorKey, longerChain, mutualHeight);
assertEquals("longer chain should have greater weight", 1, longerChainWeight.compareTo(shorterChainWeight));
if (NTP.getTime() >= BlockChain.getInstance().getCalcChainWeightTimestamp())
assertEquals("longer chain should have same weight", 0, longerChainWeight.compareTo(shorterChainWeight));
else
assertEquals("longer chain should have greater weight", 1, longerChainWeight.compareTo(shorterChainWeight));
}
}

View File

@@ -6,12 +6,12 @@ import org.qortal.block.BlockChain;
import org.qortal.crypto.BouncyCastle25519;
import org.qortal.crypto.Crypto;
import org.qortal.test.common.Common;
import org.qortal.utils.Base58;
import static org.junit.Assert.*;
import java.security.SecureRandom;
import org.bitcoinj.core.Base58;
import org.bouncycastle.crypto.agreement.X25519Agreement;
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;

View File

@@ -2,10 +2,12 @@ package org.qortal.test;
import java.awt.TrayIcon.MessageType;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.gui.SplashFrame;
import org.qortal.gui.SysTray;
@Ignore
public class GuiTests {
@Test

View File

@@ -1,5 +1,6 @@
package org.qortal.test;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.crypto.MemoryPoW;
@@ -7,6 +8,7 @@ import static org.junit.Assert.*;
import java.util.Random;
@Ignore
public class MemoryPoWTests {
private static final int workBufferLength = 8 * 1024 * 1024;

View File

@@ -0,0 +1,133 @@
package org.qortal.test;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.crosschain.BitcoinACCTv1;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.PresenceTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.PresenceTransaction;
import org.qortal.transaction.PresenceTransaction.PresenceType;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.utils.NTP;
import com.google.common.primitives.Longs;
import static org.junit.Assert.*;
public class PresenceTests extends Common {
private static final byte[] BITCOIN_PKH = new byte[20];
private static final byte[] HASH_OF_SECRET_B = new byte[32];
private PrivateKeyAccount signer;
private Repository repository;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
this.repository = RepositoryManager.getRepository();
this.signer = Common.getTestAccount(this.repository, "bob");
// We need to create corresponding test trade offer
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(this.signer.getAddress(), BITCOIN_PKH, HASH_OF_SECRET_B,
0L, 0L,
7 * 24 * 60 * 60);
long txTimestamp = NTP.getTime();
byte[] lastReference = this.signer.getLastReference();
long fee = 0;
String name = "QORT-BTC cross-chain trade";
String description = "Qortal-Bitcoin cross-chain trade";
String atType = "ACCT";
String tags = "QORT-BTC ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, this.signer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, 1L, Asset.QORT);
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndImportValid(this.repository, deployAtTransactionData, this.signer);
BlockUtils.mintBlock(this.repository);
}
@After
public void afterTest() throws DataException {
if (this.repository != null)
this.repository.close();
this.repository = null;
}
@Test
public void validityTests() throws DataException {
long timestamp = System.currentTimeMillis();
byte[] timestampBytes = Longs.toByteArray(timestamp);
byte[] timestampSignature = this.signer.sign(timestampBytes);
assertTrue(isValid(Group.NO_GROUP, this.signer, timestamp, timestampSignature));
PrivateKeyAccount nonTrader = Common.getTestAccount(repository, "alice");
assertFalse(isValid(Group.NO_GROUP, nonTrader, timestamp, timestampSignature));
}
@Test
public void newestOnlyTests() throws DataException {
long OLDER_TIMESTAMP = System.currentTimeMillis() - 2000L;
long NEWER_TIMESTAMP = OLDER_TIMESTAMP + 1000L;
PresenceTransaction older = buildPresenceTransaction(Group.NO_GROUP, this.signer, OLDER_TIMESTAMP, null);
older.computeNonce();
TransactionUtils.signAndImportValid(repository, older.getTransactionData(), this.signer);
assertTrue(this.repository.getTransactionRepository().exists(older.getTransactionData().getSignature()));
PresenceTransaction newer = buildPresenceTransaction(Group.NO_GROUP, this.signer, NEWER_TIMESTAMP, null);
newer.computeNonce();
TransactionUtils.signAndImportValid(repository, newer.getTransactionData(), this.signer);
assertTrue(this.repository.getTransactionRepository().exists(newer.getTransactionData().getSignature()));
assertFalse(this.repository.getTransactionRepository().exists(older.getTransactionData().getSignature()));
}
private boolean isValid(int txGroupId, PrivateKeyAccount signer, long timestamp, byte[] timestampSignature) throws DataException {
Transaction transaction = buildPresenceTransaction(txGroupId, signer, timestamp, timestampSignature);
return transaction.isValidUnconfirmed() == ValidationResult.OK;
}
private PresenceTransaction buildPresenceTransaction(int txGroupId, PrivateKeyAccount signer, long timestamp, byte[] timestampSignature) throws DataException {
int nonce = 0;
byte[] reference = signer.getLastReference();
byte[] creatorPublicKey = signer.getPublicKey();
long fee = 0L;
if (timestampSignature == null)
timestampSignature = this.signer.sign(Longs.toByteArray(timestamp));
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, null);
PresenceTransactionData transactionData = new PresenceTransactionData(baseTransactionData, nonce, PresenceType.TRADE_BOT, timestampSignature);
return new PresenceTransaction(this.repository, transactionData);
}
}

View File

@@ -4,7 +4,7 @@ import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.BitcoinACCTv1;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@@ -15,12 +15,18 @@ import org.qortal.test.common.Common;
import static org.junit.Assert.*;
import java.lang.reflect.Field;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -127,14 +133,139 @@ public class RepositoryTests extends Common {
}
}
@Test
public void testTrimDeadlock() {
ExecutorService executor = Executors.newCachedThreadPool();
CountDownLatch readyLatch = new CountDownLatch(1);
CountDownLatch updateLatch = new CountDownLatch(1);
CountDownLatch syncLatch = new CountDownLatch(1);
// Open connection 1
try (final HSQLDBRepository repository1 = (HSQLDBRepository) RepositoryManager.getRepository()) {
// Read AT states trim height
int atTrimHeight = repository1.getATRepository().getAtTrimHeight();
repository1.discardChanges();
// Open connection 2
try (final HSQLDBRepository repository2 = (HSQLDBRepository) RepositoryManager.getRepository()) {
// Read online signatures trim height
int onlineSignaturesTrimHeight = repository2.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
repository2.discardChanges();
Future<Boolean> f2 = executor.submit(() -> {
Object trimHeightsLock = extractTrimHeightsLock(repository2);
System.out.println(String.format("f2: repository2's trimHeightsLock object: %s", trimHeightsLock));
// Update online signatures trim height (implicit commit)
synchronized (trimHeightsLock) {
try {
System.out.println("f2: updating online signatures trim height...");
// simulate: repository2.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(onlineSignaturesTrimHeight);
String updateSql = "UPDATE DatabaseInfo SET online_signatures_trim_height = ?";
PreparedStatement pstmt = repository2.prepareStatement(updateSql);
pstmt.setInt(1, onlineSignaturesTrimHeight);
pstmt.executeUpdate();
// But no commit/saveChanges yet to force HSQLDB error
System.out.println("f2: readyLatch.countDown()");
readyLatch.countDown();
// wait for other thread to be ready to hit sync block
System.out.println("f2: waiting for f1 syncLatch...");
syncLatch.await();
// hang on to trimHeightsLock to force other thread to wait (if code is correct), or to fail (if code is faulty)
System.out.println("f2: updateLatch.await(<with timeout>)");
if (!updateLatch.await(500L, TimeUnit.MILLISECONDS)) { // long enough for other thread to reach synchronized block
// wait period expired suggesting no concurrent access, i.e. code is correct
System.out.println("f2: updateLatch.await() timed out");
System.out.println("f2: saveChanges()");
repository2.saveChanges();
return Boolean.TRUE;
}
System.out.println("f2: saveChanges()");
repository2.saveChanges();
// Early exit from wait period suggests concurrent access, i.e. code faulty
return Boolean.FALSE;
} catch (InterruptedException | SQLException e) {
System.out.println("f2: exception: " + e.getMessage());
return Boolean.FALSE;
}
}
});
System.out.println("waiting for f2 readyLatch...");
readyLatch.await();
System.out.println("launching f1...");
Future<Boolean> f1 = executor.submit(() -> {
Object trimHeightsLock = extractTrimHeightsLock(repository1);
System.out.println(String.format("f1: repository1's trimHeightsLock object: %s", trimHeightsLock));
System.out.println("f1: syncLatch.countDown()");
syncLatch.countDown();
// Update AT states trim height (implicit commit)
synchronized (trimHeightsLock) {
try {
System.out.println("f1: updating AT trim height...");
// simulate: repository1.getATRepository().setAtTrimHeight(atTrimHeight);
String updateSql = "UPDATE DatabaseInfo SET AT_trim_height = ?";
PreparedStatement pstmt = repository1.prepareStatement(updateSql);
pstmt.setInt(1, atTrimHeight);
pstmt.executeUpdate();
System.out.println("f1: saveChanges()");
repository1.saveChanges();
System.out.println("f1: updateLatch.countDown()");
updateLatch.countDown();
return Boolean.TRUE;
} catch (SQLException e) {
System.out.println("f1: exception: " + e.getMessage());
return Boolean.FALSE;
}
}
});
if (Boolean.TRUE != f1.get())
fail("concurrency bug - simultaneous update of DatabaseInfo table");
if (Boolean.TRUE != f2.get())
fail("concurrency bug - not synchronized on same object?");
} catch (InterruptedException e) {
fail("concurrency bug: " + e.getMessage());
} catch (ExecutionException e) {
fail("concurrency bug: " + e.getMessage());
}
} catch (DataException e) {
fail("database bug");
}
}
private static Object extractTrimHeightsLock(HSQLDBRepository repository) {
try {
Field trimHeightsLockField = repository.getClass().getDeclaredField("trimHeightsLock");
trimHeightsLockField.setAccessible(true);
return trimHeightsLockField.get(repository);
} catch (IllegalArgumentException | NoSuchFieldException | SecurityException | IllegalAccessException e) {
fail();
return null;
}
}
/** Check that the <i>sub-query</i> used to fetch highest block height is optimized by HSQLDB. */
@Test
public void testBlockHeightSpeed() throws DataException, SQLException {
final int mintBlockCount = 30000;
final int mintBlockCount = 10000;
try (final Repository repository = RepositoryManager.getRepository()) {
// Mint some blocks
System.out.println(String.format("Minting %d test blocks - should take approx. 30 seconds...", mintBlockCount));
System.out.println(String.format("Minting %d test blocks - should take approx. 10 seconds...", mintBlockCount));
long beforeBigMint = System.currentTimeMillis();
for (int i = 0; i < mintBlockCount; ++i)
@@ -267,7 +398,7 @@ public class RepositoryTests extends Common {
@Test
public void testAtLateral() {
try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) {
byte[] codeHash = BTCACCT.CODE_BYTES_HASH;
byte[] codeHash = BitcoinACCTv1.CODE_BYTES_HASH;
Boolean isFinished = null;
Integer dataByteOffset = null;
Long expectedValue = null;

View File

@@ -1,5 +1,6 @@
package org.qortal.test;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.data.transaction.TransactionData;
@@ -37,6 +38,7 @@ public class SerializationTests extends Common {
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testTransactions() throws DataException, TransformationException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount signingAccount = Common.getTestAccount(repository, "alice");

View File

@@ -2,6 +2,7 @@ package org.qortal.test;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
@@ -30,6 +31,7 @@ import static org.junit.Assert.*;
import java.util.List;
import java.util.Random;
@Ignore(value = "Doesn't work, to be fixed later")
public class TransferPrivsTests extends Common {
private static List<Integer> cumulativeBlocksByLevel;

View File

@@ -5,6 +5,7 @@ import static org.junit.Assert.*;
import java.util.Collections;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.api.resource.AddressesResource;
import org.qortal.test.common.ApiCommon;
@@ -24,6 +25,7 @@ public class AddressesApiTests extends ApiCommon {
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testGetOnlineAccounts() {
assertNotNull(this.addressesResource.getOnlineAccounts());
}

View File

@@ -90,11 +90,11 @@ public class BlockApiTests extends ApiCommon {
for (Integer endHeight : testValues)
for (Integer count : testValues) {
if (startHeight != null && endHeight != null && count != null) {
assertApiError(ApiError.INVALID_CRITERIA, () -> this.blocksResource.getBlockRange(startHeight, endHeight, count));
assertApiError(ApiError.INVALID_CRITERIA, () -> this.blocksResource.getBlockSummaries(startHeight, endHeight, count));
continue;
}
assertNotNull(this.blocksResource.getBlockRange(startHeight, endHeight, count));
assertNotNull(this.blocksResource.getBlockSummaries(startHeight, endHeight, count));
}
}

View File

@@ -4,10 +4,13 @@ import org.junit.Before;
import org.junit.Test;
import org.qortal.api.ApiError;
import org.qortal.api.resource.CrossChainResource;
import org.qortal.crosschain.SupportedBlockchain;
import org.qortal.test.common.ApiCommon;
public class CrossChainApiTests extends ApiCommon {
private static final SupportedBlockchain SPECIFIC_BLOCKCHAIN = null;
private CrossChainResource crossChainResource;
@Before
@@ -17,12 +20,13 @@ public class CrossChainApiTests extends ApiCommon {
@Test
public void testGetTradeOffers() {
assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getTradeOffers(limit, offset, reverse));
assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getTradeOffers(SPECIFIC_BLOCKCHAIN, limit, offset, reverse));
}
@Test
public void testGetCompletedTrades() {
assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getCompletedTrades(System.currentTimeMillis() /*minimumTimestamp*/, limit, offset, reverse));
long minimumTimestamp = System.currentTimeMillis();
assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, minimumTimestamp, limit, offset, reverse));
}
@Test
@@ -31,8 +35,8 @@ public class CrossChainApiTests extends ApiCommon {
Integer offset = null;
Boolean reverse = null;
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(-1L /*minimumTimestamp*/, limit, offset, reverse));
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(0L /*minimumTimestamp*/, limit, offset, reverse));
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, -1L /*minimumTimestamp*/, limit, offset, reverse));
assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, 0L /*minimumTimestamp*/, limit, offset, reverse));
}
}

View File

@@ -1,327 +0,0 @@
package org.qortal.test.apps;
import java.io.File;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.Transaction.SigHash;
import org.bitcoinj.core.TransactionBroadcast;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.kits.WalletAppKit;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.Script.ScriptType;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptChunk;
import org.bitcoinj.script.ScriptOpCodes;
import org.bitcoinj.wallet.WalletTransaction.Pool;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
/**
* Initiator must be Qortal-chain so that initiator can send initial message to BTC P2SH then Qortal can scan for P2SH add send corresponding message to Qortal AT.
*
* Initiator (wants QORT, has BTC)
* Funds BTC P2SH address
*
* Responder (has QORT, wants BTC)
* Builds Qortal ACCT AT and funds it with QORT
*
* Initiator sends recipient+secret+script as input to BTC P2SH address, releasing BTC amount - fees to responder
*
* Qortal nodes scan for P2SH output, checks amount and recipient and if ok sends secret to Qortal ACCT AT
* (Or it's possible to feed BTC transaction details into Qortal AT so it can check them itself?)
*
* Qortal ACCT AT sends its QORT to initiator
*
*/
public class BTCACCTTests {
private static final long TIMEOUT = 600L;
private static final Coin sendValue = Coin.valueOf(6_000L);
private static final Coin fee = Coin.valueOf(2_000L);
private static final byte[] senderPrivKeyBytes = HashCode.fromString("027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c").asBytes();
private static final byte[] recipientPrivKeyBytes = HashCode.fromString("ec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03").asBytes();
// The following need to be updated manually
private static final String prevTxHash = "70ee97f20afea916c2e7b47f6abf3c75f97c4c2251b4625419406a2dd47d16b5";
private static final Coin prevTxBalance = Coin.valueOf(562_000L); // This is NOT the amount but the unspent balance
private static final long prevTxOutputIndex = 1L;
// For when we want to re-run
private static final byte[] prevSecret = HashCode.fromString("30a13291e350214bea5318f990b77bc11d2cb709f7c39859f248bef396961dcc").asBytes();
private static final long prevLockTime = 1539347892L;
private static final boolean usePreviousFundingTx = false;
private static final boolean doRefundNotRedeem = false;
public static void main(String[] args) throws NoSuchAlgorithmException, InsufficientMoneyException, InterruptedException, ExecutionException, UnknownHostException {
Security.insertProviderAt(new BouncyCastleProvider(), 0);
byte[] secret = new byte[32];
new SecureRandom().nextBytes(secret);
if (usePreviousFundingTx)
secret = prevSecret;
System.out.println("Secret: " + HashCode.fromBytes(secret).toString());
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
byte[] secretHash = sha256Digester.digest(secret);
String secretHashHex = HashCode.fromBytes(secretHash).toString();
System.out.println("SHA256(secret): " + secretHashHex);
NetworkParameters params = TestNet3Params.get();
// NetworkParameters params = RegTestParams.get();
System.out.println("Network: " + params.getId());
WalletAppKit kit = new WalletAppKit(params, new File("."), "btc-tests");
kit.setBlockingStartup(false);
kit.startAsync();
kit.awaitRunning();
long now = System.currentTimeMillis() / 1000L;
long lockTime = now + TIMEOUT;
if (usePreviousFundingTx)
lockTime = prevLockTime;
System.out.println("LockTime: " + lockTime);
ECKey senderKey = ECKey.fromPrivate(senderPrivKeyBytes);
kit.wallet().importKey(senderKey);
ECKey recipientKey = ECKey.fromPrivate(recipientPrivKeyBytes);
kit.wallet().importKey(recipientKey);
byte[] senderPubKey = senderKey.getPubKey();
System.out.println("Sender address: " + Address.fromKey(params, senderKey, ScriptType.P2PKH).toString());
System.out.println("Sender pubkey: " + HashCode.fromBytes(senderPubKey).toString());
byte[] recipientPubKey = recipientKey.getPubKey();
System.out.println("Recipient address: " + Address.fromKey(params, recipientKey, ScriptType.P2PKH).toString());
System.out.println("Recipient pubkey: " + HashCode.fromBytes(recipientPubKey).toString());
byte[] redeemScriptBytes = buildRedeemScript(secret, senderPubKey, recipientPubKey, lockTime);
System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString());
byte[] redeemScriptHash = hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
System.out.println("P2SH address: " + p2shAddress.toString());
// Send amount to P2SH address
Transaction fundingTransaction = buildFundingTransaction(params, Sha256Hash.wrap(prevTxHash), prevTxOutputIndex, prevTxBalance, senderKey,
sendValue.add(fee), redeemScriptHash);
System.out.println("Sending " + sendValue.add(fee).toPlainString() + " to " + p2shAddress.toString());
if (!usePreviousFundingTx)
broadcastWithConfirmation(kit, fundingTransaction);
if (doRefundNotRedeem) {
// Refund
System.out.println("Refunding " + sendValue.toPlainString() + " back to " + Address.fromKey(params, senderKey, ScriptType.P2PKH).toString());
now = System.currentTimeMillis() / 1000L;
long refundLockTime = now - 60 * 30; // 30 minutes in the past, needs to before 'now' and before "median block time" (median of previous 11 block
// timestamps)
if (refundLockTime < lockTime)
throw new RuntimeException("Too soon to refund");
TransactionOutPoint fundingOutPoint = new TransactionOutPoint(params, 0, fundingTransaction);
Transaction refundTransaction = buildRefundTransaction(params, fundingOutPoint, senderKey, sendValue, redeemScriptBytes, refundLockTime);
broadcastWithConfirmation(kit, refundTransaction);
} else {
// Redeem
System.out.println("Redeeming " + sendValue.toPlainString() + " to " + Address.fromKey(params, recipientKey, ScriptType.P2PKH).toString());
TransactionOutPoint fundingOutPoint = new TransactionOutPoint(params, 0, fundingTransaction);
Transaction redeemTransaction = buildRedeemTransaction(params, fundingOutPoint, recipientKey, sendValue, secret, redeemScriptBytes);
broadcastWithConfirmation(kit, redeemTransaction);
}
kit.wallet().cleanup();
for (Transaction transaction : kit.wallet().getTransactionPool(Pool.PENDING).values())
System.out.println("Pending tx: " + transaction.getTxId().toString());
}
private static final byte[] redeemScript1 = HashCode.fromString("76a820").asBytes();
private static final byte[] redeemScript2 = HashCode.fromString("87637576a914").asBytes();
private static final byte[] redeemScript3 = HashCode.fromString("88ac6704").asBytes();
private static final byte[] redeemScript4 = HashCode.fromString("b17576a914").asBytes();
private static final byte[] redeemScript5 = HashCode.fromString("88ac68").asBytes();
private static byte[] buildRedeemScript(byte[] secret, byte[] senderPubKey, byte[] recipientPubKey, long lockTime) {
try {
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
byte[] secretHash = sha256Digester.digest(secret);
byte[] senderPubKeyHash = hash160(senderPubKey);
byte[] recipientPubKeyHash = hash160(recipientPubKey);
return Bytes.concat(redeemScript1, secretHash, redeemScript2, recipientPubKeyHash, redeemScript3, toLEByteArray((int) (lockTime & 0xffffffffL)),
redeemScript4, senderPubKeyHash, redeemScript5);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Message digest unsupported", e);
}
}
private static byte[] hash160(byte[] input) {
try {
MessageDigest rmd160Digester = MessageDigest.getInstance("RIPEMD160");
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
return rmd160Digester.digest(sha256Digester.digest(input));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Message digest unsupported", e);
}
}
private static Transaction buildFundingTransaction(NetworkParameters params, Sha256Hash prevTxHash, long outputIndex, Coin balance, ECKey sigKey, Coin value,
byte[] redeemScriptHash) {
Transaction fundingTransaction = new Transaction(params);
// Outputs (needed before input so inputs can be signed)
// Fixed amount to P2SH
fundingTransaction.addOutput(value, ScriptBuilder.createP2SHOutputScript(redeemScriptHash));
// Change to sender
fundingTransaction.addOutput(balance.minus(value).minus(fee), ScriptBuilder.createOutputScript(Address.fromKey(params, sigKey, ScriptType.P2PKH)));
// Input
// We create fake "to address" scriptPubKey for prev tx so our spending input is P2PKH type
Script fakeScriptPubKey = ScriptBuilder.createOutputScript(Address.fromKey(params, sigKey, ScriptType.P2PKH));
TransactionOutPoint prevOut = new TransactionOutPoint(params, outputIndex, prevTxHash);
fundingTransaction.addSignedInput(prevOut, fakeScriptPubKey, sigKey);
return fundingTransaction;
}
private static Transaction buildRedeemTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey recipientKey, Coin value, byte[] secret,
byte[] redeemScriptBytes) {
Transaction redeemTransaction = new Transaction(params);
redeemTransaction.setVersion(2);
// Outputs
redeemTransaction.addOutput(value, ScriptBuilder.createOutputScript(Address.fromKey(params, recipientKey, ScriptType.P2PKH)));
// Input
byte[] recipientPubKey = recipientKey.getPubKey();
ScriptBuilder scriptBuilder = new ScriptBuilder();
scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey));
scriptBuilder.addChunk(new ScriptChunk(secret.length, secret));
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
byte[] scriptPubKey = scriptBuilder.build().getProgram();
TransactionInput input = new TransactionInput(params, null, scriptPubKey, fundingOutPoint);
input.setSequenceNumber(0xffffffffL); // Final
redeemTransaction.addInput(input);
// Generate transaction signature for input
boolean anyoneCanPay = false;
Sha256Hash hash = redeemTransaction.hashForSignature(0, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
System.out.println("redeem transaction's input hash: " + hash.toString());
ECKey.ECDSASignature ecSig = recipientKey.sign(hash);
TransactionSignature txSig = new TransactionSignature(ecSig, SigHash.ALL, anyoneCanPay);
byte[] txSigBytes = txSig.encodeToBitcoin();
System.out.println("redeem transaction's signature: " + HashCode.fromBytes(txSigBytes).toString());
// Prepend signature to input
scriptBuilder.addChunk(0, new ScriptChunk(txSigBytes.length, txSigBytes));
input.setScriptSig(scriptBuilder.build());
return redeemTransaction;
}
private static Transaction buildRefundTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey senderKey, Coin value,
byte[] redeemScriptBytes, long lockTime) {
Transaction refundTransaction = new Transaction(params);
refundTransaction.setVersion(2);
// Outputs
refundTransaction.addOutput(value, ScriptBuilder.createOutputScript(Address.fromKey(params, senderKey, ScriptType.P2PKH)));
// Input
byte[] recipientPubKey = senderKey.getPubKey();
ScriptBuilder scriptBuilder = new ScriptBuilder();
scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey));
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
byte[] scriptPubKey = scriptBuilder.build().getProgram();
TransactionInput input = new TransactionInput(params, null, scriptPubKey, fundingOutPoint);
input.setSequenceNumber(0);
refundTransaction.addInput(input);
// Set locktime after input but before input signature is generated
refundTransaction.setLockTime(lockTime);
// Generate transaction signature for input
boolean anyoneCanPay = false;
Sha256Hash hash = refundTransaction.hashForSignature(0, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
System.out.println("refund transaction's input hash: " + hash.toString());
ECKey.ECDSASignature ecSig = senderKey.sign(hash);
TransactionSignature txSig = new TransactionSignature(ecSig, SigHash.ALL, anyoneCanPay);
byte[] txSigBytes = txSig.encodeToBitcoin();
System.out.println("refund transaction's signature: " + HashCode.fromBytes(txSigBytes).toString());
// Prepend signature to input
scriptBuilder.addChunk(0, new ScriptChunk(txSigBytes.length, txSigBytes));
input.setScriptSig(scriptBuilder.build());
return refundTransaction;
}
private static void broadcastWithConfirmation(WalletAppKit kit, Transaction transaction) {
System.out.println("Broadcasting tx: " + transaction.getTxId().toString());
System.out.println("TX hex: " + HashCode.fromBytes(transaction.bitcoinSerialize()).toString());
System.out.println("Number of connected peers: " + kit.peerGroup().numConnectedPeers());
TransactionBroadcast txBroadcast = kit.peerGroup().broadcastTransaction(transaction);
try {
txBroadcast.future().get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException("Transaction broadcast failed", e);
}
// wait for confirmation
System.out.println("Waiting for confirmation of tx: " + transaction.getTxId().toString());
try {
transaction.getConfidence().getDepthFuture(1).get();
} catch (CancellationException | ExecutionException | InterruptedException e) {
throw new RuntimeException("Transaction confirmation failed", e);
}
System.out.println("Confirmed tx: " + transaction.getTxId().toString());
}
/** Convert int to little-endian byte array */
private static byte[] toLEByteArray(int value) {
return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) };
}
}

View File

@@ -3,7 +3,6 @@ package org.qortal.test.apps;
import java.math.BigDecimal;
import java.security.Security;
import org.bitcoinj.core.Base58;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.block.BlockChain;
@@ -17,6 +16,7 @@ import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.settings.Settings;
import org.qortal.transform.block.BlockTransformer;
import org.qortal.utils.Base58;
import org.roaringbitmap.IntIterator;
import io.druid.extendedset.intset.ConciseSet;

View File

@@ -10,7 +10,6 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.crypto.Crypto;
import org.qortal.utils.BIP39;
import org.qortal.utils.Base58;
import com.google.common.primitives.Bytes;
@@ -44,15 +43,13 @@ public class VanityGen {
byte checksum = (byte) (hash[0] & 0xf0);
byte[] entropy132 = Bytes.concat(entropy, new byte[] { checksum });
String mnemonic = BIP39.encode(entropy132, "en");
PrivateKeyAccount account = new PrivateKeyAccount(null, hash);
if (!account.getAddress().startsWith(prefix))
continue;
System.out.println(String.format("Address: %s, public key: %s, private key: %s, mnemonic: %s",
account.getAddress(), Base58.encode(account.getPublicKey()), Base58.encode(hash), mnemonic));
System.out.println(String.format("Address: %s, public key: %s, private key: %s",
account.getAddress(), Base58.encode(account.getPublicKey()), Base58.encode(hash)));
System.out.flush();
}
}

View File

@@ -130,6 +130,9 @@ public class AtRepositoryTests extends Common {
// Trim AT state data
repository.getATRepository().prepareForAtStateTrimming();
// COMMIT to check latest AT states persist / TEMPORARY table interaction
repository.saveChanges();
repository.getATRepository().trimAtStates(2, maxHeight, 1000);
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);

View File

@@ -77,16 +77,24 @@ public class GetNextTransactionTests extends Common {
BlockUtils.mintBlock(repository);
assertTimestamp(repository, atAddress, transaction);
// Mint a few blocks, then send non-AT message, followed by AT message
// Mint a few blocks, then send non-AT message, followed by two AT messages (in same block)
for (int i = 0; i < 5; ++i)
BlockUtils.mintBlock(repository);
sendMessage(repository, deployer, data, deployer.getAddress());
transaction = sendMessage(repository, deployer, data, atAddress);
Transaction transaction1 = sendMessage(repository, deployer, data, atAddress);
Transaction transaction2 = sendMessage(repository, deployer, data, atAddress);
BlockUtils.mintBlock(repository);
// Confirm AT finds message
// Confirm AT finds first message
BlockUtils.mintBlock(repository);
assertTimestamp(repository, atAddress, transaction);
assertTimestamp(repository, atAddress, transaction1);
// Confirm AT finds second message
BlockUtils.mintBlock(repository);
assertTimestamp(repository, atAddress, transaction2);
}
}

View File

@@ -1,126 +0,0 @@
package org.qortal.test.btcacct;
import java.security.Security;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.settings.Settings;
import com.google.common.hash.HashCode;
public class BuildP2SH {
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: BuildP2SH <refund-BTC-P2PKH> <BTC-amount> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)"));
System.err.println(String.format("example: BuildP2SH "
+ "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
+ "\t0.00008642 \\\n"
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t1585920000"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 5 || args.length > 6)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Settings.fileInstance("settings-test.json");
BTC btc = BTC.getInstance();
NetworkParameters params = btc.getNetworkParameters();
Address refundBitcoinAddress = null;
Coin bitcoinAmount = null;
Address redeemBitcoinAddress = null;
byte[] secretHash = null;
int lockTime = 0;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0;
try {
refundBitcoinAddress = Address.fromString(params, args[argIndex++]);
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Refund BTC address must be in P2PKH form");
bitcoinAmount = Coin.parseCoin(args[argIndex++]);
redeemBitcoinAddress = Address.fromString(params, args[argIndex++]);
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Redeem BTC address must be in P2PKH form");
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
if (secretHash.length != 20)
usage("Hash of secret must be 20 bytes");
lockTime = Integer.parseInt(args[argIndex++]);
int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L);
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60)
usage("Locktime (seconds) should be at between 10 minutes and 1 month from now");
if (args.length > argIndex)
bitcoinFee = Coin.parseCoin(args[argIndex++]);
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
throw new RuntimeException("Repository startup issue: " + e.getMessage());
}
try (final Repository repository = RepositoryManager.getRepository()) {
System.out.println("Confirm the following is correct based on the info you've given:");
System.out.println(String.format("Refund Bitcoin address: %s", refundBitcoinAddress));
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee)));
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
System.out.println(String.format("P2SH address: %s", p2shAddress));
bitcoinAmount = bitcoinAmount.add(bitcoinFee);
// Fund P2SH
System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)",
p2shAddress.toString(), BTC.format(bitcoinAmount), BTC.format(bitcoinFee)));
System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT");
} catch (DataException e) {
throw new RuntimeException("Repository issue: " + e.getMessage());
}
}
}

View File

@@ -1,170 +0,0 @@
package org.qortal.test.btcacct;
import java.security.Security;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.settings.Settings;
import com.google.common.hash.HashCode;
public class CheckP2SH {
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: CheckP2SH <P2SH-address> <refund-BTC-P2PKH> <BTC-amount> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)"));
System.err.println(String.format("example: CheckP2SH "
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
+ "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
+ "\t0.00008642 \\\n"
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t1585920000"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 6 || args.length > 7)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Settings.fileInstance("settings-test.json");
BTC btc = BTC.getInstance();
NetworkParameters params = btc.getNetworkParameters();
Address p2shAddress = null;
Address refundBitcoinAddress = null;
Coin bitcoinAmount = null;
Address redeemBitcoinAddress = null;
byte[] secretHash = null;
int lockTime = 0;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0;
try {
p2shAddress = Address.fromString(params, args[argIndex++]);
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
usage("P2SH address invalid");
refundBitcoinAddress = Address.fromString(params, args[argIndex++]);
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Refund BTC address must be in P2PKH form");
bitcoinAmount = Coin.parseCoin(args[argIndex++]);
redeemBitcoinAddress = Address.fromString(params, args[argIndex++]);
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Redeem BTC address must be in P2PKH form");
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
if (secretHash.length != 20)
usage("Hash of secret must be 20 bytes");
lockTime = Integer.parseInt(args[argIndex++]);
int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L);
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 7 * 24 * 60 * 60)
usage("Locktime (seconds) should be at between 10 minutes and 1 week from now");
if (args.length > argIndex)
bitcoinFee = Coin.parseCoin(args[argIndex++]);
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
throw new RuntimeException("Repository startup issue: " + e.getMessage());
}
try (final Repository repository = RepositoryManager.getRepository()) {
System.out.println("Confirm the following is correct based on the info you've given:");
System.out.println(String.format("Refund Bitcoin address: %s", redeemBitcoinAddress));
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
System.out.println(String.format("Redeem Bitcoin address: %s", refundBitcoinAddress));
System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee)));
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
System.out.println(String.format("P2SH address: %s", p2shAddress));
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!derivedP2shAddress.equals(p2shAddress)) {
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
System.exit(2);
}
bitcoinAmount = bitcoinAmount.add(bitcoinFee);
long medianBlockTime = BTC.getInstance().getMedianBlockTime();
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
long now = System.currentTimeMillis();
if (now < medianBlockTime * 1000L)
System.out.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
// Check P2SH is funded
long p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
if (fundingOutputs == null) {
System.err.println(String.format("Can't find outputs for P2SH"));
System.exit(2);
}
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue())));
if (fundingOutputs.isEmpty()) {
System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
System.exit(2);
}
if (fundingOutputs.size() != 1) {
System.err.println(String.format("Expecting only one unspent output for P2SH"));
System.exit(2);
}
} catch (DataException e) {
System.err.println("Repository issue: " + e.getMessage());
} catch (BitcoinException e) {
System.err.println("Bitcoin issue: " + e.getMessage());
}
}
}

View File

@@ -1,9 +0,0 @@
package org.qortal.test.btcacct;
import org.bitcoinj.core.Coin;
public abstract class Common {
public static final Coin DEFAULT_BTC_FEE = Coin.parseCoin("0.00001000");
}

View File

@@ -1,53 +0,0 @@
package org.qortal.test.btcacct;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class P2shTests extends Common {
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
}
@After
public void afterTest() {
BTC.resetForTesting();
}
@Test
public void testFindP2shSecret() throws BitcoinException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
public void testDetermineP2shStatus() throws BitcoinException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
BTCP2SH.Status p2shStatus = BTCP2SH.determineP2shStatus(p2shAddress, 1L);
System.out.println(String.format("P2SH %s status: %s", p2shAddress, p2shStatus.name()));
}
}

View File

@@ -1,211 +0,0 @@
package org.qortal.test.btcacct;
import java.security.Security;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.settings.Settings;
import com.google.common.hash.HashCode;
public class Redeem {
static {
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
}
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: Redeem <P2SH-address> <refund-BTC-P2PKH> <redeem-BTC-PRIVATE-key> <secret> <locktime> (<BTC-redeem/refund-fee>)"));
System.err.println(String.format("example: Redeem "
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
+ "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
+ "\tec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03 \\\n"
+ "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n"
+ "\t1585920000"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 5 || args.length > 6)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Settings.fileInstance("settings-test.json");
BTC btc = BTC.getInstance();
NetworkParameters params = btc.getNetworkParameters();
Address p2shAddress = null;
Address refundBitcoinAddress = null;
byte[] redeemPrivateKey = null;
byte[] secret = null;
int lockTime = 0;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0;
try {
p2shAddress = Address.fromString(params, args[argIndex++]);
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
usage("P2SH address invalid");
refundBitcoinAddress = Address.fromString(params, args[argIndex++]);
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Refund BTC address must be in P2PKH form");
redeemPrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
// Auto-trim
if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38)
redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33);
if (redeemPrivateKey.length != 32)
usage("Redeem private key must be 32 bytes");
secret = HashCode.fromString(args[argIndex++]).asBytes();
if (secret.length == 0)
usage("Invalid secret bytes");
lockTime = Integer.parseInt(args[argIndex++]);
if (args.length > argIndex)
bitcoinFee = Coin.parseCoin(args[argIndex++]);
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
throw new RuntimeException("Repository startup issue: " + e.getMessage());
}
try (final Repository repository = RepositoryManager.getRepository()) {
System.out.println("Confirm the following is correct based on the info you've given:");
System.out.println(String.format("Redeem PRIVATE key: %s", HashCode.fromBytes(redeemPrivateKey)));
System.out.println(String.format("Redeem miner's fee: %s", BTC.format(bitcoinFee)));
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
// New/derived info
byte[] secretHash = Crypto.hash160(secret);
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey);
Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH);
System.out.println(String.format("Redeem recipient (PKH): %s (%s)", redeemAddress, HashCode.fromBytes(redeemAddress.getHash())));
System.out.println(String.format("P2SH address: %s", p2shAddress));
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!derivedP2shAddress.equals(p2shAddress)) {
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
System.exit(2);
}
// Some checks
System.out.println("\nProcessing:");
long medianBlockTime;
try {
medianBlockTime = BTC.getInstance().getMedianBlockTime();
} catch (BitcoinException e1) {
System.err.println("Unable to determine median block time");
System.exit(2);
return;
}
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
long now = System.currentTimeMillis();
if (now < medianBlockTime * 1000L) {
System.err.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
System.exit(2);
}
// Check P2SH is funded
long p2shBalance;
try {
p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
} catch (BitcoinException e) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
return;
}
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs;
try {
fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
} catch (BitcoinException e) {
System.err.println(String.format("Can't find outputs for P2SH"));
System.exit(2);
return;
}
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue())));
if (fundingOutputs.isEmpty()) {
System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
System.exit(2);
}
if (fundingOutputs.size() != 1) {
System.err.println(String.format("Expecting only one unspent output for P2SH"));
// No longer fatal
}
for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(redeemAmount), BTC.format(bitcoinFee)));
Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret, redeemAddress.getHash());
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString()));
} catch (NumberFormatException e) {
usage(String.format("Number format exception: %s", e.getMessage()));
} catch (DataException e) {
throw new RuntimeException("Repository issue: " + e.getMessage());
}
}
}

View File

@@ -1,215 +0,0 @@
package org.qortal.test.btcacct;
import java.security.Security;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.script.Script.ScriptType;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.settings.Settings;
import com.google.common.hash.HashCode;
public class Refund {
static {
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
}
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: Refund <P2SH-address> <refund-BTC-PRIVATE-KEY> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)"));
System.err.println(String.format("example: Refund "
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
+ "\tef027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c01b576fb7e \\\n"
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t1585920000"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 5 || args.length > 6)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Settings.fileInstance("settings-test.json");
BTC btc = BTC.getInstance();
NetworkParameters params = btc.getNetworkParameters();
Address p2shAddress = null;
byte[] refundPrivateKey = null;
Address redeemBitcoinAddress = null;
byte[] secretHash = null;
int lockTime = 0;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0;
try {
p2shAddress = Address.fromString(params, args[argIndex++]);
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
usage("P2SH address invalid");
refundPrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
// Auto-trim
if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38)
refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33);
if (refundPrivateKey.length != 32)
usage("Refund private key must be 32 bytes");
redeemBitcoinAddress = Address.fromString(params, args[argIndex++]);
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Their BTC address must be in P2PKH form");
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
if (secretHash.length != 20)
usage("HASH160 of secret must be 20 bytes");
lockTime = Integer.parseInt(args[argIndex++]);
if (args.length > argIndex)
bitcoinFee = Coin.parseCoin(args[argIndex++]);
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
throw new RuntimeException("Repository startup issue: " + e.getMessage());
}
try (final Repository repository = RepositoryManager.getRepository()) {
System.out.println("Confirm the following is correct based on the info you've given:");
System.out.println(String.format("Refund PRIVATE key: %s", HashCode.fromBytes(refundPrivateKey)));
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
System.out.println(String.format("P2SH address: %s", p2shAddress));
System.out.println(String.format("Refund miner's fee: %s", BTC.format(bitcoinFee)));
// New/derived info
System.out.println("\nCHECKING info from other party:");
ECKey refundKey = ECKey.fromPrivate(refundPrivateKey);
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash())));
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!derivedP2shAddress.equals(p2shAddress)) {
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
System.exit(2);
}
// Some checks
System.out.println("\nProcessing:");
long medianBlockTime;
try {
medianBlockTime = BTC.getInstance().getMedianBlockTime();
} catch (BitcoinException e) {
System.err.println("Unable to determine median block time");
System.exit(2);
return;
}
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
long now = System.currentTimeMillis();
if (now < medianBlockTime * 1000L) {
System.err.println(String.format("Too soon (%s) to refund based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
System.exit(2);
}
if (now < lockTime * 1000L) {
System.err.println(String.format("Too soon (%s) to refund based on lockTime %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC)));
System.exit(2);
}
// Check P2SH is funded
long p2shBalance;
try {
p2shBalance = BTC.getInstance().getConfirmedBalance(p2shAddress.toString());
} catch (BitcoinException e) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
return;
}
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.format(p2shBalance)));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs;
try {
fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
} catch (BitcoinException e) {
System.err.println(String.format("Can't find outputs for P2SH"));
System.exit(2);
return;
}
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.format(fundingOutput.getValue())));
if (fundingOutputs.isEmpty()) {
System.err.println(String.format("Can't refund spent/unfunded P2SH"));
System.exit(2);
}
if (fundingOutputs.size() != 1) {
System.err.println(String.format("Expecting only one unspent output for P2SH"));
// No longer fatal
}
for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee)));
Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundKey.getPubKeyHash());
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString()));
} catch (NumberFormatException e) {
usage(String.format("Number format exception: %s", e.getMessage()));
} catch (DataException e) {
throw new RuntimeException("Repository issue: " + e.getMessage());
}
}
}

View File

@@ -1,44 +1,46 @@
package org.qortal.test.btcacct;
package org.qortal.test.crosschain;
import static org.junit.Assert.*;
import java.util.Arrays;
import java.util.List;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class BtcTests extends Common {
public class BitcoinTests extends Common {
private Bitcoin bitcoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
bitcoin = Bitcoin.getInstance();
}
@After
public void afterTest() {
BTC.resetForTesting();
Bitcoin.resetForTesting();
bitcoin = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, BitcoinException {
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
System.out.println(String.format("Starting BTC instance..."));
BTC btc = BTC.getInstance();
System.out.println(String.format("BTC instance started"));
long before = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", btc.getMedianBlockTime()));
System.out.println(String.format("Bitcoin median blocktime: %d", bitcoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", btc.getMedianBlockTime()));
System.out.println(String.format("Bitcoin median blocktime: %d", bitcoin.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
@@ -51,14 +53,12 @@ public class BtcTests extends Common {
}
@Test
public void testFindP2shSecret() throws BitcoinException {
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
byte[] secret = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddress);
assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
@@ -66,52 +66,46 @@ public class BtcTests extends Common {
@Test
public void testBuildSpend() {
BTC btc = BTC.getInstance();
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = btc.buildSpend(xprv58, recipient, amount);
Transaction transaction = bitcoin.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
// Check spent key caching doesn't affect outcome
transaction = btc.buildSpend(xprv58, recipient, amount);
transaction = bitcoin.buildSpend(xprv58, recipient, amount);
assertNotNull(transaction);
}
@Test
public void testGetWalletBalance() {
BTC btc = BTC.getInstance();
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = btc.getWalletBalance(xprv58);
Long balance = bitcoin.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(BTC.format(balance));
System.out.println(bitcoin.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = btc.getWalletBalance(xprv58);
Long repeatBalance = bitcoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(BTC.format(repeatBalance));
System.out.println(bitcoin.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws BitcoinException {
BTC btc = BTC.getInstance();
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = btc.getUnusedReceiveAddress(xprv58);
String address = bitcoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);

View File

@@ -0,0 +1,114 @@
package org.qortal.test.crosschain;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Dogecoin;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import java.util.Arrays;
import static org.junit.Assert.*;
public class DogecoinTests extends Common {
private Dogecoin dogecoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
dogecoin = Dogecoin.getInstance();
}
@After
public void afterTest() {
Dogecoin.resetForTesting();
dogecoin = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", dogecoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", dogecoin.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(dogecoin, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = dogecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = dogecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test
public void testGetWalletBalance() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = dogecoin.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(dogecoin.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = dogecoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(dogecoin.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = dogecoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}

View File

@@ -1,9 +1,11 @@
package org.qortal.test.btcacct;
package org.qortal.test.crosschain;
import static org.junit.Assert.*;
import java.security.Security;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import org.bitcoinj.core.Address;
import org.bitcoinj.params.TestNet3Params;
@@ -11,11 +13,13 @@ import org.bitcoinj.script.ScriptBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.junit.Test;
import org.qortal.crosschain.BitcoinException;
import org.qortal.crosschain.BitcoinTransaction;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.BitcoinyTransaction;
import org.qortal.crosschain.ElectrumX;
import org.qortal.crosschain.TransactionHash;
import org.qortal.crosschain.UnspentOutput;
import org.qortal.crosschain.Bitcoin.BitcoinNet;
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode;
@@ -30,15 +34,25 @@ public class ElectrumXTests {
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
}
private static final Map<ElectrumX.Server.ConnectionType, Integer> DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ElectrumX.Server.ConnectionType.class);
static {
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001);
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002);
}
private ElectrumX getInstance() {
return new ElectrumX("Bitcoin-" + BitcoinNet.TEST3.name(), BitcoinNet.TEST3.getGenesisHash(), BitcoinNet.TEST3.getServers(), DEFAULT_ELECTRUMX_PORTS);
}
@Test
public void testInstance() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
ElectrumX electrumX = getInstance();
assertNotNull(electrumX);
}
@Test
public void testGetCurrentHeight() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
public void testGetCurrentHeight() throws ForeignBlockchainException {
ElectrumX electrumX = getInstance();
int height = electrumX.getCurrentHeight();
@@ -48,10 +62,10 @@ public class ElectrumXTests {
@Test
public void testInvalidRequest() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
ElectrumX electrumX = getInstance();
try {
electrumX.getBlockHeaders(-1, -1);
} catch (BitcoinException e) {
electrumX.getRawBlockHeaders(-1, -1);
} catch (ForeignBlockchainException e) {
// Should throw due to negative start block height
return;
}
@@ -60,13 +74,13 @@ public class ElectrumXTests {
}
@Test
public void testGetRecentBlocks() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
public void testGetRecentBlocks() throws ForeignBlockchainException {
ElectrumX electrumX = getInstance();
int height = electrumX.getCurrentHeight();
assertTrue(height > 10000);
List<byte[]> recentBlockHeaders = electrumX.getBlockHeaders(height - 11, 11);
List<byte[]> recentBlockHeaders = electrumX.getRawBlockHeaders(height - 11, 11);
System.out.println(String.format("Returned %d recent blocks", recentBlockHeaders.size()));
for (int i = 0; i < recentBlockHeaders.size(); ++i) {
@@ -80,8 +94,8 @@ public class ElectrumXTests {
}
@Test
public void testGetP2PKHBalance() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
public void testGetP2PKHBalance() throws ForeignBlockchainException {
ElectrumX electrumX = getInstance();
Address address = Address.fromString(TestNet3Params.get(), "n3GNqMveyvaPvUbH469vDRadqpJMPc84JA");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
@@ -93,8 +107,8 @@ public class ElectrumXTests {
}
@Test
public void testGetP2SHBalance() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
public void testGetP2SHBalance() throws ForeignBlockchainException {
ElectrumX electrumX = getInstance();
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
@@ -106,8 +120,8 @@ public class ElectrumXTests {
}
@Test
public void testGetUnspentOutputs() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
public void testGetUnspentOutputs() throws ForeignBlockchainException {
ElectrumX electrumX = getInstance();
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
@@ -120,8 +134,8 @@ public class ElectrumXTests {
}
@Test
public void testGetRawTransaction() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
public void testGetRawTransaction() throws ForeignBlockchainException {
ElectrumX electrumX = getInstance();
byte[] txHash = HashCode.fromString("7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af").asBytes();
@@ -132,26 +146,26 @@ public class ElectrumXTests {
@Test
public void testGetUnknownRawTransaction() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
ElectrumX electrumX = getInstance();
byte[] txHash = HashCode.fromString("f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0").asBytes();
try {
electrumX.getRawTransaction(txHash);
fail("Bitcoin transaction should be unknown and hence throw exception");
} catch (BitcoinException e) {
if (!(e instanceof BitcoinException.NotFoundException))
} catch (ForeignBlockchainException e) {
if (!(e instanceof ForeignBlockchainException.NotFoundException))
fail("Bitcoin transaction should be unknown and hence throw NotFoundException");
}
}
@Test
public void testGetTransaction() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
public void testGetTransaction() throws ForeignBlockchainException {
ElectrumX electrumX = getInstance();
String txHash = "7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af";
BitcoinTransaction transaction = electrumX.getTransaction(txHash);
BitcoinyTransaction transaction = electrumX.getTransaction(txHash);
assertNotNull(transaction);
assertTrue(transaction.txHash.equals(txHash));
@@ -159,22 +173,22 @@ public class ElectrumXTests {
@Test
public void testGetUnknownTransaction() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
ElectrumX electrumX = getInstance();
String txHash = "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0";
try {
electrumX.getTransaction(txHash);
fail("Bitcoin transaction should be unknown and hence throw exception");
} catch (BitcoinException e) {
if (!(e instanceof BitcoinException.NotFoundException))
} catch (ForeignBlockchainException e) {
if (!(e instanceof ForeignBlockchainException.NotFoundException))
fail("Bitcoin transaction should be unknown and hence throw NotFoundException");
}
}
@Test
public void testGetAddressTransactions() throws BitcoinException {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
public void testGetAddressTransactions() throws ForeignBlockchainException {
ElectrumX electrumX = getInstance();
Address address = Address.fromString(TestNet3Params.get(), "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();

View File

@@ -0,0 +1,128 @@
package org.qortal.test.crosschain;
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crypto.Crypto;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
import com.google.common.primitives.Longs;
public class HtlcTests extends Common {
private Bitcoin bitcoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
bitcoin = Bitcoin.getInstance();
}
@After
public void afterTest() {
Bitcoin.resetForTesting();
bitcoin = null;
}
@Test
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddress);
assertNotNull(secret);
assertArrayEquals("secret incorrect", expectedSecret, secret);
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testHtlcSecretCaching() throws ForeignBlockchainException {
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
do {
// We need to perform fresh setup for 1st test
Bitcoin.resetForTesting();
bitcoin = Bitcoin.getInstance();
long now = System.currentTimeMillis();
long timestampBoundary = now / 30_000L;
byte[] secret1 = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddress);
long executionPeriod1 = System.currentTimeMillis() - now;
assertNotNull(secret1);
assertArrayEquals("secret1 incorrect", expectedSecret, secret1);
assertTrue("1st execution period should not be instant!", executionPeriod1 > 10);
byte[] secret2 = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddress);
long executionPeriod2 = System.currentTimeMillis() - now - executionPeriod1;
assertNotNull(secret2);
assertArrayEquals("secret2 incorrect", expectedSecret, secret2);
// Test is only valid if we've called within same timestampBoundary
if (System.currentTimeMillis() / 30_000L != timestampBoundary)
continue;
assertArrayEquals(secret1, secret2);
assertTrue("2st execution period should be effectively instant!", executionPeriod2 < 10);
} while (false);
}
@Test
public void testDetermineHtlcStatus() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
BitcoinyHTLC.Status htlcStatus = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddress, 1L);
assertNotNull(htlcStatus);
System.out.println(String.format("HTLC %s status: %s", p2shAddress, htlcStatus.name()));
}
@Test
public void testHtlcStatusCaching() throws ForeignBlockchainException {
do {
// We need to perform fresh setup for 1st test
Bitcoin.resetForTesting();
bitcoin = Bitcoin.getInstance();
long now = System.currentTimeMillis();
long timestampBoundary = now / 30_000L;
// Won't ever exist
String p2shAddress = bitcoin.deriveP2shAddress(Crypto.hash160(Longs.toByteArray(now)));
BitcoinyHTLC.Status htlcStatus1 = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddress, 1L);
long executionPeriod1 = System.currentTimeMillis() - now;
assertNotNull(htlcStatus1);
assertTrue("1st execution period should not be instant!", executionPeriod1 > 10);
BitcoinyHTLC.Status htlcStatus2 = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddress, 1L);
long executionPeriod2 = System.currentTimeMillis() - now - executionPeriod1;
assertNotNull(htlcStatus2);
assertEquals(htlcStatus1, htlcStatus2);
// Test is only valid if we've called within same timestampBoundary
if (System.currentTimeMillis() / 30_000L != timestampBoundary)
continue;
assertTrue("2st execution period should be effectively instant!", executionPeriod2 < 10);
} while (false);
}
}

View File

@@ -0,0 +1,114 @@
package org.qortal.test.crosschain;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Litecoin;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class LitecoinTests extends Common {
private Litecoin litecoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
litecoin = Litecoin.getInstance();
}
@After
public void afterTest() {
Litecoin.resetForTesting();
litecoin = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", litecoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", litecoin.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(litecoin, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = litecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = litecoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test
public void testGetWalletBalance() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
Long balance = litecoin.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(litecoin.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = litecoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(litecoin.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String address = litecoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}

View File

@@ -0,0 +1,114 @@
package org.qortal.test.crosschain.apps;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.script.Script.ScriptType;
import org.qortal.crosschain.Litecoin;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.BitcoinyHTLC;
import com.google.common.hash.HashCode;
public class BuildHTLC {
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: BuildHTLC (-b | -l) <refund-P2PKH> <amount> <redeem-P2PKH> <HASH160-of-secret> <locktime>"));
System.err.println("where: -b means use Bitcoin, -l means use Litecoin");
System.err.println(String.format("example: BuildHTLC -l "
+ "msAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n"
+ "\t0.00008642 \\\n"
+ "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t1600000000"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 6 || args.length > 6)
usage(null);
Common.init();
Bitcoiny bitcoiny = null;
NetworkParameters params = null;
Address refundAddress = null;
Coin amount = null;
Address redeemAddress = null;
byte[] hashOfSecret = null;
int lockTime = 0;
int argIndex = 0;
try {
switch (args[argIndex++]) {
case "-b":
bitcoiny = Bitcoin.getInstance();
break;
case "-l":
bitcoiny = Litecoin.getInstance();
break;
default:
usage("Only Bitcoin (-b) or Litecoin (-l) supported");
}
params = bitcoiny.getNetworkParameters();
refundAddress = Address.fromString(params, args[argIndex++]);
if (refundAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Refund address must be in P2PKH form");
amount = Coin.parseCoin(args[argIndex++]);
redeemAddress = Address.fromString(params, args[argIndex++]);
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Redeem address must be in P2PKH form");
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
if (hashOfSecret.length != 20)
usage("Hash of secret must be 20 bytes");
lockTime = Integer.parseInt(args[argIndex++]);
int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L);
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60)
usage("Locktime (seconds) should be at between 10 minutes and 1 month from now");
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId()));
Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny));
if (p2shFee.isZero())
return;
System.out.println(String.format("Refund address: %s", refundAddress));
System.out.println(String.format("Amount: %s", amount.toPlainString()));
System.out.println(String.format("Redeem address: %s", redeemAddress));
System.out.println(String.format("Refund/redeem miner's fee: %s", bitcoiny.format(p2shFee)));
System.out.println(String.format("Script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(hashOfSecret)));
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), hashOfSecret);
System.out.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes)));
String p2shAddress = bitcoiny.deriveP2shAddress(redeemScriptBytes);
System.out.println(String.format("P2SH address: %s", p2shAddress));
amount = amount.add(p2shFee);
// Fund P2SH
System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)",
p2shAddress, bitcoiny.format(amount), bitcoiny.format(p2shFee)));
}
}

View File

@@ -0,0 +1,135 @@
package org.qortal.test.crosschain.apps;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.script.Script.ScriptType;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.Litecoin;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crypto.Crypto;
import com.google.common.hash.HashCode;
public class CheckHTLC {
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: CheckHTLC (-b | -l) <P2SH-address> <refund-P2PKH> <amount> <redeem-P2PKH> <HASH160-of-secret> <locktime>"));
System.err.println("where: -b means use Bitcoin, -l means use Litecoin");
System.err.println(String.format("example: CheckP2SH -l "
+ "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n"
+ "msAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n"
+ "\t0.00008642 \\\n"
+ "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t1600184800"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 7 || args.length > 7)
usage(null);
Common.init();
Bitcoiny bitcoiny = null;
NetworkParameters params = null;
Address p2shAddress = null;
Address refundAddress = null;
Coin amount = null;
Address redeemAddress = null;
byte[] hashOfSecret = null;
int lockTime = 0;
int argIndex = 0;
try {
switch (args[argIndex++]) {
case "-b":
bitcoiny = Bitcoin.getInstance();
break;
case "-l":
bitcoiny = Litecoin.getInstance();
break;
default:
usage("Only Bitcoin (-b) or Litecoin (-l) supported");
}
params = bitcoiny.getNetworkParameters();
p2shAddress = Address.fromString(params, args[argIndex++]);
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
usage("P2SH address invalid");
refundAddress = Address.fromString(params, args[argIndex++]);
if (refundAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Refund address must be in P2PKH form");
amount = Coin.parseCoin(args[argIndex++]);
redeemAddress = Address.fromString(params, args[argIndex++]);
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Redeem address must be in P2PKH form");
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
if (hashOfSecret.length != 20)
usage("Hash of secret must be 20 bytes");
lockTime = Integer.parseInt(args[argIndex++]);
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId()));
Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny));
if (p2shFee.isZero())
return;
System.out.println(String.format("P2SH address: %s", p2shAddress));
System.out.println(String.format("Refund PKH: %s", refundAddress));
System.out.println(String.format("Redeem/refund amount: %s", amount.toPlainString()));
System.out.println(String.format("Redeem PKH: %s", redeemAddress));
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(hashOfSecret)));
System.out.println(String.format("Script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
System.out.println(String.format("Redeem/refund miner's fee: %s", bitcoiny.format(p2shFee)));
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), hashOfSecret);
System.out.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!derivedP2shAddress.equals(p2shAddress)) {
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
System.exit(2);
}
amount = amount.add(p2shFee);
// Check network's median block time
int medianBlockTime = Common.checkMedianBlockTime(bitcoiny, null);
if (medianBlockTime == 0)
return;
// Check P2SH is funded
Common.getBalance(bitcoiny, p2shAddress.toString());
// Grab all unspent outputs
Common.getUnspentOutputs(bitcoiny, p2shAddress.toString());
Common.determineHtlcStatus(bitcoiny, p2shAddress.toString(), amount.value);
}
}

View File

@@ -0,0 +1,158 @@
package org.qortal.test.crosschain.apps;
import java.security.Security;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Collections;
import java.util.List;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.settings.Settings;
import org.qortal.utils.NTP;
import com.google.common.hash.HashCode;
public abstract class Common {
public static void init() {
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
Settings.fileInstance("settings-test.json");
NTP.setFixedOffset(0L);
}
public static long getP2shFee(Bitcoiny bitcoiny) {
long p2shFee;
try {
p2shFee = bitcoiny.getP2shFee(null);
} catch (ForeignBlockchainException e) {
System.err.println(String.format("Unable to determine P2SH fee: %s", e.getMessage()));
return 0;
}
return p2shFee;
}
public static int checkMedianBlockTime(Bitcoiny bitcoiny, Integer lockTime) {
int medianBlockTime;
try {
medianBlockTime = bitcoiny.getMedianBlockTime();
} catch (ForeignBlockchainException e) {
System.err.println(String.format("Unable to determine median block time: %s", e.getMessage()));
return 0;
}
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
long now = System.currentTimeMillis();
if (now < medianBlockTime * 1000L) {
System.out.println(String.format("Too soon (%s) based on median block time %s",
LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC),
LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
return 0;
}
if (lockTime != null && now < lockTime * 1000L) {
System.err.println(String.format("Too soon (%s) based on lockTime %s",
LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC),
LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC)));
return 0;
}
return medianBlockTime;
}
public static long getBalance(Bitcoiny bitcoiny, String address58) {
long balance;
try {
balance = bitcoiny.getConfirmedBalance(address58);
} catch (ForeignBlockchainException e) {
System.err.println(String.format("Unable to check address %s balance: %s", address58, e.getMessage()));
return 0;
}
System.out.println(String.format("Address %s balance: %s", address58, bitcoiny.format(balance)));
return balance;
}
public static List<TransactionOutput> getUnspentOutputs(Bitcoiny bitcoiny, String address58) {
List<TransactionOutput> unspentOutputs = Collections.emptyList();
try {
unspentOutputs = bitcoiny.getUnspentOutputs(address58);
} catch (ForeignBlockchainException e) {
System.err.println(String.format("Can't find unspent outputs for %s: %s", address58, e.getMessage()));
return unspentOutputs;
}
System.out.println(String.format("Found %d output%s for %s",
unspentOutputs.size(),
(unspentOutputs.size() != 1 ? "s" : ""),
address58));
for (TransactionOutput fundingOutput : unspentOutputs)
System.out.println(String.format("Output %s:%d amount %s",
HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(),
bitcoiny.format(fundingOutput.getValue())));
if (unspentOutputs.isEmpty())
System.err.println(String.format("Can't use spent/unfunded %s", address58));
if (unspentOutputs.size() != 1)
System.err.println(String.format("Expecting only one unspent output?"));
return unspentOutputs;
}
public static BitcoinyHTLC.Status determineHtlcStatus(Bitcoiny bitcoiny, String address58, long minimumAmount) {
BitcoinyHTLC.Status htlcStatus = null;
try {
htlcStatus = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), address58, minimumAmount);
System.out.println(String.format("HTLC status: %s", htlcStatus.name()));
} catch (ForeignBlockchainException e) {
System.err.println(String.format("Unable to determine HTLC status: %s", e.getMessage()));
}
return htlcStatus;
}
public static void broadcastTransaction(Bitcoiny bitcoiny, Transaction transaction) {
byte[] rawTransactionBytes = transaction.bitcoinSerialize();
System.out.println(String.format("%nRaw transaction bytes:%n%s%n", HashCode.fromBytes(rawTransactionBytes).toString()));
for (int countDown = 5; countDown >= 1; --countDown) {
System.out.print(String.format("\rBroadcasting transaction in %d second%s... use CTRL-C to abort ", countDown, (countDown != 1 ? "s" : "")));
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
System.exit(0);
}
}
System.out.println("Broadcasting transaction... ");
try {
bitcoiny.broadcastTransaction(transaction);
} catch (ForeignBlockchainException e) {
System.err.println(String.format("Failed to broadcast transaction: %s", e.getMessage()));
System.exit(1);
}
}
}

View File

@@ -0,0 +1,78 @@
package org.qortal.test.crosschain.apps;
import java.security.Security;
import org.bitcoinj.core.AddressFormatException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Litecoin;
import org.qortal.settings.Settings;
public class GetNextReceiveAddress {
static {
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
}
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: GetNextReceiveAddress (-b | -l) <xprv/xpub>"));
System.err.println(String.format("example (testnet): GetNextReceiveAddress -l tpubD6NzVbkrYhZ4X3jV96Wo3Kr8Au2v9cUUEmPRk1smwduFrRVfBjkkw49rRYjgff1fGSktFMfabbvv8b1dmfyLjjbDax6QGyxpsNsx5PXukCB"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length != 2)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
Settings.fileInstance("settings-test.json");
Bitcoiny bitcoiny = null;
String key58 = null;
int argIndex = 0;
try {
switch (args[argIndex++]) {
case "-b":
bitcoiny = Bitcoin.getInstance();
break;
case "-l":
bitcoiny = Litecoin.getInstance();
break;
default:
usage("Only Bitcoin (-b) or Litecoin (-l) supported");
}
key58 = args[argIndex++];
if (!bitcoiny.isValidDeterministicKey(key58))
usage("Not valid xprv/xpub/tprv/tpub");
} catch (NumberFormatException | AddressFormatException e) {
usage(String.format("Argument format exception: %s", e.getMessage()));
}
System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId()));
String receiveAddress = null;
try {
receiveAddress = bitcoiny.getUnusedReceiveAddress(key58);
} catch (ForeignBlockchainException e) {
System.err.println(String.format("Failed to determine next receive address: %s", e.getMessage()));
System.exit(1);
}
System.out.println(String.format("Next receive address: %s", receiveAddress));
}
}

View File

@@ -1,4 +1,4 @@
package org.qortal.test.btcacct;
package org.qortal.test.crosschain.apps;
import java.security.Security;
import java.util.List;
@@ -6,8 +6,11 @@ import java.util.List;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.TransactionOutput;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BitcoinException;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Litecoin;
import org.qortal.settings.Settings;
import com.google.common.hash.HashCode;
@@ -23,34 +26,51 @@ public class GetTransaction {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: GetTransaction <bitcoin-tx>"));
System.err.println(String.format("example (mainnet): GetTransaction 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660"));
System.err.println(String.format("example (testnet): GetTransaction 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e"));
System.err.println(String.format("usage: GetTransaction (-b | -l) <tx-hash>"));
System.err.println(String.format("example (mainnet): GetTransaction -b 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660"));
System.err.println(String.format("example (testnet): GetTransaction -b 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length != 1)
if (args.length != 2)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
Settings.fileInstance("settings-test.json");
Bitcoiny bitcoiny = null;
byte[] transactionId = null;
int argIndex = 0;
try {
int argIndex = 0;
switch (args[argIndex++]) {
case "-b":
bitcoiny = Bitcoin.getInstance();
break;
case "-l":
bitcoiny = Litecoin.getInstance();
break;
default:
usage("Only Bitcoin (-b) or Litecoin (-l) supported");
}
transactionId = HashCode.fromString(args[argIndex++]).asBytes();
} catch (NumberFormatException | AddressFormatException e) {
usage(String.format("Argument format exception: %s", e.getMessage()));
}
System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId()));
// Grab all outputs from transaction
List<TransactionOutput> fundingOutputs;
try {
fundingOutputs = BTC.getInstance().getOutputs(transactionId);
} catch (BitcoinException e) {
fundingOutputs = bitcoiny.getOutputs(transactionId);
} catch (ForeignBlockchainException e) {
System.out.println(String.format("Transaction not found (or error occurred)"));
return;
}

View File

@@ -0,0 +1,82 @@
package org.qortal.test.crosschain.apps;
import java.security.Security;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import org.bitcoinj.core.AddressFormatException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.crosschain.*;
import org.qortal.settings.Settings;
public class GetWalletTransactions {
static {
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
}
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: GetWalletTransactions (-b | -l) <xprv/xpub>"));
System.err.println(String.format("example (testnet): GetWalletTransactions -l tpubD6NzVbkrYhZ4X3jV96Wo3Kr8Au2v9cUUEmPRk1smwduFrRVfBjkkw49rRYjgff1fGSktFMfabbvv8b1dmfyLjjbDax6QGyxpsNsx5PXukCB"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length != 2)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
Settings.fileInstance("settings-test.json");
Bitcoiny bitcoiny = null;
String key58 = null;
int argIndex = 0;
try {
switch (args[argIndex++]) {
case "-b":
bitcoiny = Bitcoin.getInstance();
break;
case "-l":
bitcoiny = Litecoin.getInstance();
break;
default:
usage("Only Bitcoin (-b) or Litecoin (-l) supported");
}
key58 = args[argIndex++];
if (!bitcoiny.isValidDeterministicKey(key58))
usage("Not valid xprv/xpub/tprv/tpub");
} catch (NumberFormatException | AddressFormatException e) {
usage(String.format("Argument format exception: %s", e.getMessage()));
}
System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId()));
// Grab all outputs from transaction
List<SimpleTransaction> transactions = null;
try {
transactions = bitcoiny.getWalletTransactions(key58);
} catch (ForeignBlockchainException e) {
System.err.println(String.format("Failed to obtain wallet transactions: %s", e.getMessage()));
System.exit(1);
}
System.out.println(String.format("Found %d transaction%s", transactions.size(), (transactions.size() != 1 ? "s" : "")));
for (SimpleTransaction transaction : transactions.stream().sorted(Comparator.comparingInt(SimpleTransaction::getTimestamp)).collect(Collectors.toList()))
System.out.println(String.format("%s", transaction));
}
}

View File

@@ -0,0 +1,80 @@
package org.qortal.test.crosschain.apps;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.Litecoin;
public class Pay {
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: Pay (-b | -l) <xprv58> <recipient> <LTC-amount>"));
System.err.println("where: -b means use Bitcoin, -l means use Litecoin");
System.err.println(String.format("example: Pay -l "
+ "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ \\\n"
+ "\tmsAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n"
+ "\t0.00008642"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 4 || args.length > 4)
usage(null);
Common.init();
Bitcoiny bitcoiny = null;
NetworkParameters params = null;
String xprv58 = null;
Address address = null;
Coin amount = null;
int argIndex = 0;
try {
switch (args[argIndex++]) {
case "-b":
bitcoiny = Bitcoin.getInstance();
break;
case "-l":
bitcoiny = Litecoin.getInstance();
break;
default:
usage("Only Bitcoin (-b) or Litecoin (-l) supported");
}
params = bitcoiny.getNetworkParameters();
xprv58 = args[argIndex++];
if (!bitcoiny.isValidDeterministicKey(xprv58))
usage("xprv invalid");
address = Address.fromString(params, args[argIndex++]);
amount = Coin.parseCoin(args[argIndex++]);
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId()));
System.out.println(String.format("Address: %s", address));
System.out.println(String.format("Amount: %s", amount.toPlainString()));
Transaction transaction = bitcoiny.buildSpend(xprv58, address.toString(), amount.value);
if (transaction == null) {
System.err.println("Insufficent funds");
System.exit(1);
}
Common.broadcastTransaction(bitcoiny, transaction);
}
}

View File

@@ -0,0 +1,166 @@
package org.qortal.test.crosschain.apps;
import java.util.Arrays;
import java.util.List;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.script.Script.ScriptType;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.Litecoin;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crypto.Crypto;
import com.google.common.hash.HashCode;
public class RedeemHTLC {
static {
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
}
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: Redeem (-b | -l) <P2SH-address> <refund-P2PKH> <redeem-PRIVATE-key> <secret> <locktime> <output-address>"));
System.err.println("where: -b means use Bitcoin, -l means use Litecoin");
System.err.println(String.format("example: Redeem -l "
+ "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n"
+ "\tmsAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n"
+ "\tefdaed23c4bc85c8ccae40d774af3c2a10391c648b6420cdd83cd44c27fcb5955201c64e372d \\\n"
+ "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n"
+ "\t1600184800 \\\n"
+ "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 7 || args.length > 7)
usage(null);
Common.init();
Bitcoiny bitcoiny = null;
NetworkParameters params = null;
Address p2shAddress = null;
Address refundAddress = null;
byte[] redeemPrivateKey = null;
byte[] secret = null;
int lockTime = 0;
Address outputAddress = null;
int argIndex = 0;
try {
switch (args[argIndex++]) {
case "-b":
bitcoiny = Bitcoin.getInstance();
break;
case "-l":
bitcoiny = Litecoin.getInstance();
break;
default:
usage("Only Bitcoin (-b) or Litecoin (-l) supported");
}
params = bitcoiny.getNetworkParameters();
p2shAddress = Address.fromString(params, args[argIndex++]);
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
usage("P2SH address invalid");
refundAddress = Address.fromString(params, args[argIndex++]);
if (refundAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Refund address must be in P2PKH form");
redeemPrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
// Auto-trim
if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38)
redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33);
if (redeemPrivateKey.length != 32)
usage("Redeem private key must be 32 bytes");
secret = HashCode.fromString(args[argIndex++]).asBytes();
if (secret.length == 0)
usage("Invalid secret bytes");
lockTime = Integer.parseInt(args[argIndex++]);
outputAddress = Address.fromString(params, args[argIndex++]);
if (outputAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Output address invalid");
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId()));
Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny));
if (p2shFee.isZero())
return;
System.out.println(String.format("Attempting to redeem HTLC %s to %s", p2shAddress, outputAddress));
byte[] hashOfSecret = Crypto.hash160(secret);
ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey);
Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH);
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), hashOfSecret);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!derivedP2shAddress.equals(p2shAddress)) {
System.err.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes)));
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
System.exit(2);
return;
}
// Actual live processing...
int medianBlockTime = Common.checkMedianBlockTime(bitcoiny, null);
if (medianBlockTime == 0)
return;
// Check P2SH is funded
long p2shBalance = Common.getBalance(bitcoiny, p2shAddress.toString());
if (p2shBalance == 0)
return;
// Grab all unspent outputs
List<TransactionOutput> unspentOutputs = Common.getUnspentOutputs(bitcoiny, p2shAddress.toString());
if (unspentOutputs.isEmpty())
return;
Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(p2shFee);
BitcoinyHTLC.Status htlcStatus = Common.determineHtlcStatus(bitcoiny, p2shAddress.toString(), redeemAmount.value);
if (htlcStatus == null)
return;
if (htlcStatus != BitcoinyHTLC.Status.FUNDED) {
System.err.println(String.format("Expecting %s HTLC status, but actual status is %s", "FUNDED", htlcStatus.name()));
System.exit(2);
return;
}
System.out.println(String.format("Spending %s of outputs, with %s as mining fee", bitcoiny.format(redeemAmount), bitcoiny.format(p2shFee)));
Transaction redeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoiny.getNetworkParameters(), redeemAmount, redeemKey,
unspentOutputs, redeemScriptBytes, secret, outputAddress.getHash());
Common.broadcastTransaction(bitcoiny, redeemTransaction);
}
}

View File

@@ -0,0 +1,163 @@
package org.qortal.test.crosschain.apps;
import java.util.Arrays;
import java.util.List;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.script.Script.ScriptType;
import org.qortal.crosschain.Litecoin;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.crypto.Crypto;
import com.google.common.hash.HashCode;
public class RefundHTLC {
static {
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
}
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: RefundHTLC (-b | -l) <P2SH-address> <refund-PRIVATE-KEY> <redeem-P2PKH> <HASH160-of-secret> <locktime> <output-address>"));
System.err.println("where: -b means use Bitcoin, -l means use Litecoin");
System.err.println(String.format("example: RefundHTLC -l "
+ "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n"
+ "\tef8f31b49c31b4a140aebcd9605fded88cc2dad0844c4b984f9191a5a416f72d3801e16447b0 \\\n"
+ "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t1600184800 \\\n"
+ "\tmoJtbbhs7T4Z5hmBH2iyKhGrCWBzQWS2CL"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 7 || args.length > 7)
usage(null);
Common.init();
Bitcoiny bitcoiny = null;
NetworkParameters params = null;
Address p2shAddress = null;
byte[] refundPrivateKey = null;
Address redeemAddress = null;
byte[] hashOfSecret = null;
int lockTime = 0;
Address outputAddress = null;
int argIndex = 0;
try {
switch (args[argIndex++]) {
case "-b":
bitcoiny = Bitcoin.getInstance();
break;
case "-l":
bitcoiny = Litecoin.getInstance();
break;
default:
usage("Only Bitcoin (-b) or Litecoin (-l) supported");
}
params = bitcoiny.getNetworkParameters();
p2shAddress = Address.fromString(params, args[argIndex++]);
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
usage("P2SH address invalid");
refundPrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
// Auto-trim
if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38)
refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33);
if (refundPrivateKey.length != 32)
usage("Refund private key must be 32 bytes");
redeemAddress = Address.fromString(params, args[argIndex++]);
if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Redeem address must be in P2PKH form");
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
if (hashOfSecret.length != 20)
usage("HASH160 of secret must be 20 bytes");
lockTime = Integer.parseInt(args[argIndex++]);
outputAddress = Address.fromString(params, args[argIndex++]);
if (outputAddress.getOutputScriptType() != ScriptType.P2PKH)
usage("Output address invalid");
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId()));
Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny));
if (p2shFee.isZero())
return;
System.out.println(String.format("Attempting to refund HTLC %s to %s", p2shAddress, outputAddress));
ECKey refundKey = ECKey.fromPrivate(refundPrivateKey);
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), hashOfSecret);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!derivedP2shAddress.equals(p2shAddress)) {
System.err.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes)));
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
System.exit(2);
}
// Actual live processing...
int medianBlockTime = Common.checkMedianBlockTime(bitcoiny, lockTime);
if (medianBlockTime == 0)
return;
// Check P2SH is funded
long p2shBalance = Common.getBalance(bitcoiny, p2shAddress.toString());
if (p2shBalance == 0)
return;
// Grab all unspent outputs
List<TransactionOutput> unspentOutputs = Common.getUnspentOutputs(bitcoiny, p2shAddress.toString());
if (unspentOutputs.isEmpty())
return;
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(p2shFee);
BitcoinyHTLC.Status htlcStatus = Common.determineHtlcStatus(bitcoiny, p2shAddress.toString(), refundAmount.value);
if (htlcStatus == null)
return;
if (htlcStatus != BitcoinyHTLC.Status.FUNDED) {
System.err.println(String.format("Expecting %s HTLC status, but actual status is %s", "FUNDED", htlcStatus.name()));
System.exit(2);
return;
}
System.out.println(String.format("Spending %s of outputs, with %s as mining fee", bitcoiny.format(refundAmount), bitcoiny.format(p2shFee)));
Transaction refundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey,
unspentOutputs, redeemScriptBytes, lockTime, outputAddress.getHash());
Common.broadcastTransaction(bitcoiny, refundTransaction);
}
}

View File

@@ -1,4 +1,4 @@
package org.qortal.test.btcacct;
package org.qortal.test.crosschain.bitcoinv1;
import static org.junit.Assert.*;
@@ -18,7 +18,8 @@ import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.BitcoinACCTv1;
import org.qortal.crosschain.AcctMode;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
@@ -41,7 +42,7 @@ import org.qortal.utils.Amounts;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
public class AtTests extends Common {
public class BitcoinACCTv1Tests extends Common {
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
@@ -51,7 +52,7 @@ public class AtTests extends Common {
public static final int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long bitcoinAmount = 864200L;
public static final long bitcoinAmount = 864200L; // 0.00864200 BTC
private static final Random RANDOM = new Random();
@@ -64,8 +65,10 @@ public class AtTests extends Common {
public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = BTCACCT.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
}
@Test
@@ -136,7 +139,7 @@ public class AtTests extends Common {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = BTCACCT.buildCancelMessage(deployer.getAddress());
byte[] messageData = BitcoinACCTv1.getInstance().buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
@@ -150,8 +153,8 @@ public class AtTests extends Common {
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode);
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
@@ -209,8 +212,8 @@ public class AtTests extends Common {
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.CANCELLED, tradeData.mode);
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
}
@@ -232,10 +235,10 @@ public class AtTests extends Common {
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
@@ -247,10 +250,10 @@ public class AtTests extends Common {
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
@@ -259,7 +262,7 @@ public class AtTests extends Common {
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's Bitcoin PKH was extracted correctly
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerBitcoinPKH));
assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerForeignPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
@@ -293,10 +296,10 @@ public class AtTests extends Common {
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository);
@@ -309,10 +312,10 @@ public class AtTests extends Common {
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(BTCACCT.Mode.OFFERING, tradeData.mode);
assertEquals(AcctMode.OFFERING, tradeData.mode);
}
}
@@ -334,10 +337,10 @@ public class AtTests extends Common {
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
@@ -356,8 +359,8 @@ public class AtTests extends Common {
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.REFUNDED, tradeData.mode);
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
@@ -388,17 +391,17 @@ public class AtTests extends Common {
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secrets to AT, from correct account
messageData = BTCACCT.buildRedeemMessage(secretA, secretB, partner.getAddress());
messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
@@ -412,8 +415,8 @@ public class AtTests extends Common {
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.REDEEMED, tradeData.mode);
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
@@ -459,17 +462,17 @@ public class AtTests extends Common {
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secrets to AT, but from wrong account
messageData = BTCACCT.buildRedeemMessage(secretA, secretB, partner.getAddress());
messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
@@ -483,8 +486,8 @@ public class AtTests extends Common {
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
@@ -517,10 +520,10 @@ public class AtTests extends Common {
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
@@ -529,7 +532,7 @@ public class AtTests extends Common {
// Send incorrect secrets to AT, from correct account
byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret);
messageData = BTCACCT.buildRedeemMessage(wrongSecret, secretB, partner.getAddress());
messageData = BitcoinACCTv1.buildRedeemMessage(wrongSecret, secretB, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
@@ -543,8 +546,8 @@ public class AtTests extends Common {
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
@@ -552,7 +555,7 @@ public class AtTests extends Common {
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Send incorrect secrets to AT, from correct account
messageData = BTCACCT.buildRedeemMessage(secretA, wrongSecret, partner.getAddress());
messageData = BitcoinACCTv1.buildRedeemMessage(secretA, wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
@@ -565,8 +568,8 @@ public class AtTests extends Common {
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() * 2;
@@ -597,10 +600,10 @@ public class AtTests extends Common {
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int lockTimeB = BTCACCT.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = BTCACCT.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
@@ -621,8 +624,8 @@ public class AtTests extends Common {
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
assertEquals(BTCACCT.Mode.TRADING, tradeData.mode);
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
}
}
@@ -654,7 +657,7 @@ public class AtTests extends Common {
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, BTCACCT.CODE_BYTES_HASH))
if (!Arrays.equals(codeHash, BitcoinACCTv1.CODE_BYTES_HASH))
continue;
describeAt(repository, atAddress);
@@ -667,7 +670,7 @@ public class AtTests extends Common {
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = BTCACCT.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
@@ -744,7 +747,7 @@ public class AtTests extends Common {
private void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = BTCACCT.populateTradeData(repository, atData);
CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
@@ -760,17 +763,17 @@ public class AtTests extends Common {
+ "\texpected bitcoin: %s BTC,\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode.name(),
tradeData.mode,
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
atData.getIsFinished(),
HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedBitcoin),
Amounts.prettyAmount(tradeData.expectedForeignAmount),
currentBlockHeight));
if (tradeData.mode != BTCACCT.Mode.OFFERING && tradeData.mode != BTCACCT.Mode.CANCELLED) {
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\tBitcoin P2SH-A nLockTime: %d (%s),\n"

View File

@@ -1,12 +1,15 @@
package org.qortal.test.btcacct;
package org.qortal.test.crosschain.bitcoinv1;
import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.Bitcoin;
import org.qortal.crosschain.BitcoinACCTv1;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.TransactionData;
@@ -16,7 +19,7 @@ import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.settings.Settings;
import org.qortal.test.crosschain.apps.Common;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transform.TransformationException;
@@ -28,20 +31,18 @@ import com.google.common.hash.HashCode;
public class DeployAT {
public static final long atFundingExtra = 2000000L;
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <your Bitcoin PKH> <HASH160-of-secret> <AT funding amount> <trade-timeout>"));
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <AT funding amount> <BTC amount> <your Bitcoin PKH/P2PKH> <HASH160-of-secret> <trade-timeout>"));
System.err.println(String.format("example: DeployAT "
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
+ "\t80.4020 \\\n"
+ "7Eztjz2TsxwbrWUYEaSdLbASKQGTfK2rR7ViFc5gaiZw \\\n"
+ "\t10 \\\n"
+ "\t10.1 \\\n"
+ "\t0.00864200 \\\n"
+ "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb \\\n"
+ "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb (or mrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h) \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t123.456 \\\n"
+ "\t10080"));
System.exit(1);
}
@@ -50,15 +51,17 @@ public class DeployAT {
if (args.length != 7)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Settings.fileInstance("settings-test.json");
Common.init();
Bitcoiny bitcoiny = Bitcoin.getInstance();
NetworkParameters params = bitcoiny.getNetworkParameters();
byte[] refundPrivateKey = null;
long redeemAmount = 0;
long fundingAmount = 0;
long expectedBitcoin = 0;
byte[] bitcoinPublicKeyHash = null;
byte[] secretHash = null;
long fundingAmount = 0;
byte[] hashOfSecret = null;
int tradeTimeout = 0;
int argIndex = 0;
@@ -71,22 +74,30 @@ public class DeployAT {
if (redeemAmount <= 0)
usage("QORT amount must be positive");
fundingAmount = Long.parseLong(args[argIndex++]);
if (fundingAmount <= redeemAmount)
usage("AT funding amount must be greater than QORT redeem amount");
expectedBitcoin = Long.parseLong(args[argIndex++]);
if (expectedBitcoin <= 0)
usage("Expected BTC amount must be positive");
bitcoinPublicKeyHash = HashCode.fromString(args[argIndex++]).asBytes();
String bitcoinPKHish = args[argIndex++];
// Try P2PKH first
try {
Address bitcoinAddress = LegacyAddress.fromBase58(params, bitcoinPKHish);
bitcoinPublicKeyHash = bitcoinAddress.getHash();
} catch (AddressFormatException e) {
// Try parsing as PKH hex string instead
bitcoinPublicKeyHash = HashCode.fromString(bitcoinPKHish).asBytes();
}
if (bitcoinPublicKeyHash.length != 20)
usage("Bitcoin PKH must be 20 bytes");
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
if (secretHash.length != 20)
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
if (hashOfSecret.length != 20)
usage("Hash of secret must be 20 bytes");
fundingAmount = Long.parseLong(args[argIndex++]);
if (fundingAmount <= redeemAmount)
usage("AT funding amount must be greater than QORT redeem amount");
tradeTimeout = Integer.parseInt(args[argIndex++]);
if (tradeTimeout < 60 || tradeTimeout > 50000)
usage("Trade timeout (minutes) must be between 60 and 50000");
@@ -98,12 +109,11 @@ public class DeployAT {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
throw new RuntimeException("Repository startup issue: " + e.getMessage());
System.err.println(String.format("Repository start-up issue: %s", e.getMessage()));
System.exit(2);
}
try (final Repository repository = RepositoryManager.getRepository()) {
System.out.println("Confirm the following is correct based on the info you've given:");
PrivateKeyAccount refundAccount = new PrivateKeyAccount(repository, refundPrivateKey);
System.out.println(String.format("Refund Qortal address: %s", refundAccount.getAddress()));
@@ -111,11 +121,11 @@ public class DeployAT {
System.out.println(String.format("AT funding amount: %s", Amounts.prettyAmount(fundingAmount)));
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(hashOfSecret)));
// Deploy AT
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin, tradeTimeout);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecret, redeemAmount, expectedBitcoin, tradeTimeout);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = refundAccount.getLastReference();
@@ -149,11 +159,10 @@ public class DeployAT {
System.exit(2);
}
System.out.println(String.format("\nSigned transaction in base58, ready for POST /transactions/process:\n%s\n", Base58.encode(signedBytes)));
} catch (NumberFormatException e) {
usage(String.format("Number format exception: %s", e.getMessage()));
System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes)));
} catch (DataException e) {
throw new RuntimeException("Repository issue: " + e.getMessage());
System.err.println(String.format("Repository issue: %s", e.getMessage()));
System.exit(2);
}
}

View File

@@ -0,0 +1,150 @@
package org.qortal.test.crosschain.litecoinv1;
import java.math.BigDecimal;
import org.bitcoinj.core.ECKey;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.controller.Controller;
import org.qortal.crosschain.LitecoinACCTv1;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.test.crosschain.apps.Common;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Amounts;
import org.qortal.utils.Base58;
import com.google.common.hash.HashCode;
public class DeployAT {
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <AT funding amount> <LTC amount> <trade-timeout>"));
System.err.println("A trading key-pair will be generated for you!");
System.err.println(String.format("example: DeployAT "
+ "7Eztjz2TsxwbrWUYEaSdLbASKQGTfK2rR7ViFc5gaiZw \\\n"
+ "\t10 \\\n"
+ "\t10.1 \\\n"
+ "\t0.00864200 \\\n"
+ "\t120"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length != 5)
usage(null);
Common.init();
byte[] creatorPrivateKey = null;
long redeemAmount = 0;
long fundingAmount = 0;
long expectedLitecoin = 0;
int tradeTimeout = 0;
int argIndex = 0;
try {
creatorPrivateKey = Base58.decode(args[argIndex++]);
if (creatorPrivateKey.length != 32)
usage("Refund private key must be 32 bytes");
redeemAmount = new BigDecimal(args[argIndex++]).setScale(8).unscaledValue().longValue();
if (redeemAmount <= 0)
usage("QORT amount must be positive");
fundingAmount = new BigDecimal(args[argIndex++]).setScale(8).unscaledValue().longValue();
if (fundingAmount <= redeemAmount)
usage("AT funding amount must be greater than QORT redeem amount");
expectedLitecoin = new BigDecimal(args[argIndex++]).setScale(8).unscaledValue().longValue();
if (expectedLitecoin <= 0)
usage("Expected LTC amount must be positive");
tradeTimeout = Integer.parseInt(args[argIndex++]);
if (tradeTimeout < 60 || tradeTimeout > 50000)
usage("Trade timeout (minutes) must be between 60 and 50000");
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
System.err.println(String.format("Repository start-up issue: %s", e.getMessage()));
System.exit(2);
}
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount creatorAccount = new PrivateKeyAccount(repository, creatorPrivateKey);
System.out.println(String.format("Creator Qortal address: %s", creatorAccount.getAddress()));
System.out.println(String.format("QORT redeem amount: %s", Amounts.prettyAmount(redeemAmount)));
System.out.println(String.format("AT funding amount: %s", Amounts.prettyAmount(fundingAmount)));
// Generate trading key-pair
byte[] tradePrivateKey = new ECKey().getPrivKeyBytes();
PrivateKeyAccount tradeAccount = new PrivateKeyAccount(repository, tradePrivateKey);
byte[] litecoinPublicKeyHash = ECKey.fromPrivate(tradePrivateKey).getPubKeyHash();
System.out.println(String.format("Trade private key: %s", HashCode.fromBytes(tradePrivateKey)));
// Deploy AT
byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, expectedLitecoin, tradeTimeout);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = creatorAccount.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", creatorAccount.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-LTC cross-chain trade";
String description = String.format("Qortal-Litecoin cross-chain trade");
String atType = "ACCT";
String tags = "QORT-LTC ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, creatorAccount.getPublicKey(), fee, null);
DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
deployAtTransaction.sign(creatorAccount);
byte[] signedBytes = null;
try {
signedBytes = TransactionTransformer.toBytes(deployAtTransactionData);
} catch (TransformationException e) {
System.err.println(String.format("Unable to convert transaction to base58: %s", e.getMessage()));
System.exit(2);
}
DeployAtTransaction.ensureATAddress(deployAtTransactionData);
String atAddress = deployAtTransactionData.getAtAddress();
System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes)));
System.out.println(String.format("AT address: %s", atAddress));
} catch (DataException e) {
System.err.println(String.format("Repository issue: %s", e.getMessage()));
System.exit(2);
}
}
}

View File

@@ -0,0 +1,770 @@
package org.qortal.test.crosschain.litecoinv1;
import static org.junit.Assert.*;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.LitecoinACCTv1;
import org.qortal.crosschain.AcctMode;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.utils.Amounts;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
public class LitecoinACCTv1Tests extends Common {
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final byte[] litecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
public static final int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long litecoinAmount = 864200L; // 0.00864200 LTC
private static final Random RANDOM = new Random();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
}
@Test
public void testDeploy() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = fundingAmount;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = deployersInitialBalance;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = 0;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testOfferCancel() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = LitecoinACCTv1.getInstance().buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testOfferCancelInvalidLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Instead of sending creator's address to AT, send too-short/invalid message
byte[] messageData = new byte[7];
RANDOM.nextBytes(messageData);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testTradingInfoProcessing() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's Litecoin PKH was extracted correctly
assertTrue(Arrays.equals(litecoinPublicKeyHash, tradeData.partnerForeignPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
@SuppressWarnings("unused")
@Test
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(AcctMode.OFFERING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testAutomaticTradeRefund() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
// Check refund
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account
messageData = LitecoinACCTv1.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account
messageData = LitecoinACCTv1.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send incorrect secret to AT, from correct account
byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret);
messageData = LitecoinACCTv1.buildRedeemMessage(wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
messageData = Bytes.concat(secretA);
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
for (ATData atData : executableAts) {
String atAddress = atData.getATAddress();
byte[] codeBytes = atData.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
atAddress,
codeBytes.length,
(codeBytes.length != 1 ? "s": ""),
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, LitecoinACCTv1.CODE_BYTES_HASH))
continue;
describeAt(repository, atAddress);
}
}
}
private int calcTestLockTimeA(long messageTimestamp) {
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAddress, litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-LTC cross-chain trade";
String description = String.format("Qortal-Litecoin cross-chain trade");
String atType = "ACCT";
String tags = "QORT-LTC ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout / 2 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
BlockUtils.mintBlock(repository);
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
}
private void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
+ "\tis finished: %b,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected Litecoin: %s LTC,\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode,
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
atData.getIsFinished(),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedForeignAmount),
currentBlockHeight));
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund timeout: %d minutes,\n"
+ "\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\tLitecoin P2SH-A nLockTime: %d (%s),\n"
+ "\ttrade partner: %s\n"
+ "\tpartner's receiving address: %s",
tradeData.refundTimeout,
tradeData.tradeRefundHeight,
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
tradeData.qortalPartnerAddress,
tradeData.qortalPartnerReceivingAddress));
}
}
private PrivateKeyAccount createTradeAccount(Repository repository) {
// We actually use a known test account with funds to avoid PoW compute
return Common.getTestAccount(repository, "alice");
}
}

View File

@@ -0,0 +1,90 @@
package org.qortal.test.crosschain.litecoinv1;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.controller.Controller;
import org.qortal.crosschain.LitecoinACCTv1;
import org.qortal.crypto.Crypto;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.test.crosschain.apps.Common;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Base58;
public class SendCancelMessage {
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: SendCancelMessage <your Qortal PRIVATE key> <AT address>"));
System.err.println(String.format("example: SendCancelMessage "
+ "7Eztjz2TsxwbrWUYEaSdLbASKQGTfK2rR7ViFc5gaiZw \\\n"
+ "\tAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length != 2)
usage(null);
Common.init();
byte[] qortalPrivateKey = null;
String atAddress = null;
int argIndex = 0;
try {
qortalPrivateKey = Base58.decode(args[argIndex++]);
if (qortalPrivateKey.length != 32)
usage("Refund private key must be 32 bytes");
atAddress = args[argIndex++];
if (!Crypto.isValidAtAddress(atAddress))
usage("Invalid AT address");
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
System.err.println(String.format("Repository start-up issue: %s", e.getMessage()));
System.exit(2);
}
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount qortalAccount = new PrivateKeyAccount(repository, qortalPrivateKey);
String creatorQortalAddress = qortalAccount.getAddress();
System.out.println(String.format("Qortal address: %s", creatorQortalAddress));
byte[] messageData = LitecoinACCTv1.getInstance().buildCancelMessage(creatorQortalAddress);
MessageTransaction messageTransaction = MessageTransaction.build(repository, qortalAccount, Group.NO_GROUP, atAddress, messageData, false, false);
System.out.println("Computing nonce...");
messageTransaction.computeNonce();
messageTransaction.sign(qortalAccount);
byte[] signedBytes = null;
try {
signedBytes = TransactionTransformer.toBytes(messageTransaction.getTransactionData());
} catch (TransformationException e) {
System.err.println(String.format("Unable to convert transaction to bytes: %s", e.getMessage()));
System.exit(2);
}
System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes)));
} catch (DataException e) {
System.err.println(String.format("Repository issue: %s", e.getMessage()));
System.exit(2);
}
}
}

View File

@@ -0,0 +1,101 @@
package org.qortal.test.crosschain.litecoinv1;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.controller.Controller;
import org.qortal.crosschain.LitecoinACCTv1;
import org.qortal.crypto.Crypto;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.test.crosschain.apps.Common;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Base58;
import com.google.common.hash.HashCode;
public class SendRedeemMessage {
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: SendRedeemMessage <partner trade PRIVATE key> <AT address> <secret> <Qortal receive address>"));
System.err.println(String.format("example: SendRedeemMessage "
+ "dbfe739f5a3ecf7b0a22cea71f73d86ec71355b740e5972bcdf9e8bb4721ab9d \\\n"
+ "\tAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n"
+ "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n"
+ "\tQqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length != 4)
usage(null);
Common.init();
byte[] tradePrivateKey = null;
String atAddress = null;
byte[] secret = null;
String receiveAddress = null;
int argIndex = 0;
try {
tradePrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
if (tradePrivateKey.length != 32)
usage("Refund private key must be 32 bytes");
atAddress = args[argIndex++];
if (!Crypto.isValidAtAddress(atAddress))
usage("Invalid AT address");
secret = HashCode.fromString(args[argIndex++]).asBytes();
if (secret.length != 32)
usage("Secret must be 32 bytes");
receiveAddress = args[argIndex++];
if (!Crypto.isValidAddress(receiveAddress))
usage("Invalid Qortal receive address");
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
System.err.println(String.format("Repository start-up issue: %s", e.getMessage()));
System.exit(2);
}
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount tradeAccount = new PrivateKeyAccount(repository, tradePrivateKey);
byte[] messageData = LitecoinACCTv1.buildRedeemMessage(secret, receiveAddress);
MessageTransaction messageTransaction = MessageTransaction.build(repository, tradeAccount, Group.NO_GROUP, atAddress, messageData, false, false);
System.out.println("Computing nonce...");
messageTransaction.computeNonce();
messageTransaction.sign(tradeAccount);
byte[] signedBytes = null;
try {
signedBytes = TransactionTransformer.toBytes(messageTransaction.getTransactionData());
} catch (TransformationException e) {
System.err.println(String.format("Unable to convert transaction to bytes: %s", e.getMessage()));
System.exit(2);
}
System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes)));
} catch (DataException e) {
System.err.println(String.format("Repository issue: %s", e.getMessage()));
System.exit(2);
}
}
}

View File

@@ -0,0 +1,118 @@
package org.qortal.test.crosschain.litecoinv1;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.controller.Controller;
import org.qortal.crosschain.LitecoinACCTv1;
import org.qortal.crypto.Crypto;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
import org.qortal.repository.RepositoryManager;
import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.test.crosschain.apps.Common;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import com.google.common.hash.HashCode;
public class SendTradeMessage {
private static void usage(String error) {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: SendTradeMessage <trade PRIVATE key> <AT address> <partner trade Qortal address> <partner tradeLitecoin PKH/P2PKH> <hash-of-secret> <locktime>"));
System.err.println(String.format("example: SendTradeMessage "
+ "ed77aa2c62d785a9428725fc7f95b907be8a1cc43213239876a62cf70fdb6ecb \\\n"
+ "\tAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n"
+ "\tQqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq \\\n"
+ "\tffffffffffffffffffffffffffffffffffffffff \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t1600184800"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length != 6)
usage(null);
Common.init();
byte[] tradePrivateKey = null;
String atAddress = null;
String partnerTradeAddress = null;
byte[] partnerTradePublicKeyHash = null;
byte[] hashOfSecret = null;
int lockTime = 0;
int argIndex = 0;
try {
tradePrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
if (tradePrivateKey.length != 32)
usage("Refund private key must be 32 bytes");
atAddress = args[argIndex++];
if (!Crypto.isValidAtAddress(atAddress))
usage("Invalid AT address");
partnerTradeAddress = args[argIndex++];
if (!Crypto.isValidAddress(partnerTradeAddress))
usage("Invalid partner trade Qortal address");
partnerTradePublicKeyHash = HashCode.fromString(args[argIndex++]).asBytes();
if (partnerTradePublicKeyHash.length != 20)
usage("Partner trade PKH must be 20 bytes");
hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes();
if (hashOfSecret.length != 20)
usage("HASH160 of secret must be 20 bytes");
lockTime = Integer.parseInt(args[argIndex++]);
} catch (IllegalArgumentException e) {
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
}
try {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
RepositoryManager.setRepositoryFactory(repositoryFactory);
} catch (DataException e) {
System.err.println(String.format("Repository start-up issue: %s", e.getMessage()));
System.exit(2);
}
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount tradeAccount = new PrivateKeyAccount(repository, tradePrivateKey);
int refundTimeout = LitecoinACCTv1.calcRefundTimeout(NTP.getTime(), lockTime);
if (refundTimeout < 1) {
System.err.println("Refund timeout too small. Is locktime in the past?");
System.exit(2);
}
byte[] messageData = LitecoinACCTv1.buildTradeMessage(partnerTradeAddress, partnerTradePublicKeyHash, hashOfSecret, lockTime, refundTimeout);
MessageTransaction messageTransaction = MessageTransaction.build(repository, tradeAccount, Group.NO_GROUP, atAddress, messageData, false, false);
System.out.println("Computing nonce...");
messageTransaction.computeNonce();
messageTransaction.sign(tradeAccount);
byte[] signedBytes = null;
try {
signedBytes = TransactionTransformer.toBytes(messageTransaction.getTransactionData());
} catch (TransformationException e) {
System.err.println(String.format("Unable to convert transaction to bytes: %s", e.getMessage()));
System.exit(2);
}
System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes)));
} catch (DataException e) {
System.err.println(String.format("Repository issue: %s", e.getMessage()));
System.exit(2);
}
}
}

View File

@@ -7,7 +7,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.bitcoinj.core.Base58;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -25,9 +26,10 @@ import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TestAccount;
import org.qortal.utils.Amounts;
import org.qortal.utils.Base58;
public class RewardTests extends Common {
private static final Logger LOGGER = LogManager.getLogger(RewardTests.class);
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
@@ -130,19 +132,19 @@ public class RewardTests extends Common {
/*
* Example:
*
*
* Block reward is 100 QORT, QORA-holders' share is 0.20 (20%) = 20 QORT
*
*
* We hold 100 QORA
* Someone else holds 28 QORA
* Total QORA held: 128 QORA
*
*
* Our portion of that is 100 QORA / 128 QORA * 20 QORT = 15.625 QORT
*
*
* QORA holders earn at most 1 QORT per 250 QORA held.
*
*
* So we can earn at most 100 QORA / 250 QORAperQORT = 0.4 QORT
*
*
* Thus our block earning should be capped to 0.4 QORT.
*/
@@ -289,7 +291,7 @@ public class RewardTests extends Common {
* Dilbert is only account 'online'.
* No founders online.
* Some legacy QORA holders.
*
*
* So Dilbert should receive 100% - legacy QORA holder's share.
*/
@@ -336,4 +338,462 @@ public class RewardTests extends Common {
}
}
}
/** Test rewards for level 1 and 2 accounts both pre and post the shareBinFix, including orphaning back through the feature trigger block */
@Test
public void testLevel1And2Rewards() throws DataException {
Common.useSettings("test-settings-v2-reward-levels.json");
try (final Repository repository = RepositoryManager.getRepository()) {
List<PrivateKeyAccount> mintingAndOnlineAccounts = new ArrayList<>();
// Alice self share online
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
mintingAndOnlineAccounts.add(aliceSelfShare);
byte[] chloeRewardSharePrivateKey;
// Bob self-share NOT online
// Chloe self share online
try {
chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0);
} catch (IllegalArgumentException ex) {
LOGGER.error("FAILED {}", ex.getLocalizedMessage(), ex);
throw ex;
}
PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey);
mintingAndOnlineAccounts.add(chloeRewardShareAccount);
// Dilbert self share online
byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0);
PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey);
mintingAndOnlineAccounts.add(dilbertRewardShareAccount);
// Mint a couple of blocks so that we are able to orphan them later
for (int i=0; i<2; i++)
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
// Ensure that the levels are as we expect
assertEquals(1, (int) Common.getTestAccount(repository, "alice").getLevel());
assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel());
assertEquals(1, (int) Common.getTestAccount(repository, "chloe").getLevel());
assertEquals(2, (int) Common.getTestAccount(repository, "dilbert").getLevel());
// Ensure that only Alice is a founder
assertEquals(1, getFlags(repository, "alice"));
assertEquals(0, getFlags(repository, "bob"));
assertEquals(0, getFlags(repository, "chloe"));
assertEquals(0, getFlags(repository, "dilbert"));
// Now that everyone is at level 1 or 2, we can capture initial balances
Map<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT);
final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT);
final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT);
final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT);
// Mint a block
final long blockReward = BlockUtils.getNextBlockReward(repository);
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
// Ensure we are at the correct height and block reward value
assertEquals(6, (int) repository.getBlockRepository().getLastBlock().getHeight());
assertEquals(10000000000L, blockReward);
/*
* Alice, Chloe, and Dilbert are 'online'. Bob is offline.
* Chloe is level 1, Dilbert is level 2.
* One founder online (Alice, who is also level 1).
* No legacy QORA holders.
*
* Chloe and Dilbert should receive equal shares of the 5% block reward for Level 1 and 2
* Alice should receive the remainder (95%)
*/
// We are after the shareBinFix feature trigger, so we expect level 1 and 2 to share the same reward (5%)
final int level1And2SharePercent = 5_00; // 5%
final long level1And2ShareAmount = (blockReward * level1And2SharePercent) / 100L / 100L;
final long expectedReward = level1And2ShareAmount / 2; // The reward is split between Chloe and Dilbert
final long expectedFounderReward = blockReward - level1And2ShareAmount; // Alice should receive the remainder
// Validate the balances to ensure that the correct post-shareBinFix distribution is being applied
assertEquals(500000000, level1And2ShareAmount);
AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward);
AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedReward);
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedReward);
// Now orphan the latest block. This brings us to the threshold of the shareBinFix feature trigger.
BlockUtils.orphanBlocks(repository, 1);
assertEquals(5, (int) repository.getBlockRepository().getLastBlock().getHeight());
// Ensure the latest post-fix block rewards have been subtracted and they have returned to their initial values
AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance);
AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance);
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance);
// Orphan another block. This time, the block that was orphaned was prior to the shareBinFix feature trigger.
BlockUtils.orphanBlocks(repository, 1);
assertEquals(4, (int) repository.getBlockRepository().getLastBlock().getHeight());
// Prior to the fix, the levels were incorrectly grouped
// Chloe should receive 100% of the level 1 reward, and Dilbert should receive 100% of the level 2+3 reward
final int level1SharePercent = 5_00; // 5%
final int level2And3SharePercent = 10_00; // 10%
final long level1ShareAmountBeforeFix = (blockReward * level1SharePercent) / 100L / 100L;
final long level2And3ShareAmountBeforeFix = (blockReward * level2And3SharePercent) / 100L / 100L;
final long expectedFounderRewardBeforeFix = blockReward - level1ShareAmountBeforeFix - level2And3ShareAmountBeforeFix; // Alice should receive the remainder
// Validate the share amounts and balances
assertEquals(500000000, level1ShareAmountBeforeFix);
assertEquals(1000000000, level2And3ShareAmountBeforeFix);
AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance-expectedFounderRewardBeforeFix);
AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance-level1ShareAmountBeforeFix);
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance-level2And3ShareAmountBeforeFix);
// Orphan the latest block one last time
BlockUtils.orphanBlocks(repository, 1);
assertEquals(3, (int) repository.getBlockRepository().getLastBlock().getHeight());
// Validate balances
AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance-(expectedFounderRewardBeforeFix*2));
AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance-(level1ShareAmountBeforeFix*2));
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance-(level2And3ShareAmountBeforeFix*2));
}
}
/** Test rewards for level 3 and 4 accounts */
@Test
public void testLevel3And4Rewards() throws DataException {
Common.useSettings("test-settings-v2-reward-levels.json");
try (final Repository repository = RepositoryManager.getRepository()) {
List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
List<PrivateKeyAccount> mintingAndOnlineAccounts = new ArrayList<>();
// Alice self share online
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
mintingAndOnlineAccounts.add(aliceSelfShare);
// Bob self-share online
byte[] bobRewardSharePrivateKey = AccountUtils.rewardShare(repository, "bob", "bob", 0);
PrivateKeyAccount bobRewardShareAccount = new PrivateKeyAccount(repository, bobRewardSharePrivateKey);
mintingAndOnlineAccounts.add(bobRewardShareAccount);
// Chloe self share online
byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0);
PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey);
mintingAndOnlineAccounts.add(chloeRewardShareAccount);
// Dilbert self share online
byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0);
PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey);
mintingAndOnlineAccounts.add(dilbertRewardShareAccount);
// Mint enough blocks to bump testAccount levels to 3 and 4
final int minterBlocksNeeded = cumulativeBlocksByLevel.get(4) - 20; // 20 blocks before level 4, so that the test accounts reach the correct levels
for (int bc = 0; bc < minterBlocksNeeded; ++bc)
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
// Ensure that the levels are as we expect
assertEquals(3, (int) Common.getTestAccount(repository, "alice").getLevel());
assertEquals(3, (int) Common.getTestAccount(repository, "bob").getLevel());
assertEquals(3, (int) Common.getTestAccount(repository, "chloe").getLevel());
assertEquals(4, (int) Common.getTestAccount(repository, "dilbert").getLevel());
// Now that everyone is at level 3 or 4, we can capture initial balances
Map<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT);
final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT);
final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT);
final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT);
// Mint a block
final long blockReward = BlockUtils.getNextBlockReward(repository);
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
// Ensure we are using the correct block reward value
assertEquals(100000000L, blockReward);
/*
* Alice, Bob, Chloe, and Dilbert are 'online'.
* Bob and Chloe are level 3; Dilbert is level 4.
* One founder online (Alice, who is also level 3).
* No legacy QORA holders.
*
* Chloe, Bob and Dilbert should receive equal shares of the 10% block reward for level 3 and 4
* Alice should receive the remainder (90%)
*/
// We are after the shareBinFix feature trigger, so we expect level 3 and 4 to share the same reward (10%)
final int level3And4SharePercent = 10_00; // 10%
final long level3And4ShareAmount = (blockReward * level3And4SharePercent) / 100L / 100L;
final long expectedReward = level3And4ShareAmount / 3; // The reward is split between Bob, Chloe, and Dilbert
final long expectedFounderReward = blockReward - level3And4ShareAmount; // Alice should receive the remainder
// Validate the balances to ensure that the correct post-shareBinFix distribution is being applied
AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward);
AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance+expectedReward);
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedReward);
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedReward);
}
}
/** Test rewards for level 5 and 6 accounts */
@Test
public void testLevel5And6Rewards() throws DataException {
Common.useSettings("test-settings-v2-reward-levels.json");
try (final Repository repository = RepositoryManager.getRepository()) {
List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
List<PrivateKeyAccount> mintingAndOnlineAccounts = new ArrayList<>();
// Alice self share online
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
mintingAndOnlineAccounts.add(aliceSelfShare);
// Bob self-share not initially online
// Chloe self share online
byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0);
PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey);
mintingAndOnlineAccounts.add(chloeRewardShareAccount);
// Dilbert self share online
byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0);
PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey);
mintingAndOnlineAccounts.add(dilbertRewardShareAccount);
// Mint enough blocks to bump testAccount levels to 5 and 6
final int minterBlocksNeeded = cumulativeBlocksByLevel.get(6) - 20; // 20 blocks before level 6, so that the test accounts reach the correct levels
for (int bc = 0; bc < minterBlocksNeeded; ++bc)
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
// Bob self-share now comes online
byte[] bobRewardSharePrivateKey = AccountUtils.rewardShare(repository, "bob", "bob", 0);
PrivateKeyAccount bobRewardShareAccount = new PrivateKeyAccount(repository, bobRewardSharePrivateKey);
mintingAndOnlineAccounts.add(bobRewardShareAccount);
// Ensure that the levels are as we expect
assertEquals(5, (int) Common.getTestAccount(repository, "alice").getLevel());
assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel());
assertEquals(5, (int) Common.getTestAccount(repository, "chloe").getLevel());
assertEquals(6, (int) Common.getTestAccount(repository, "dilbert").getLevel());
// Now that everyone is at level 5 or 6 (except Bob who has only just started minting, so is at level 1), we can capture initial balances
Map<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT);
final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT);
final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT);
final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT);
// Mint a block
final long blockReward = BlockUtils.getNextBlockReward(repository);
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
// Ensure we are using the correct block reward value
assertEquals(100000000L, blockReward);
/*
* Alice, Bob, Chloe, and Dilbert are 'online'.
* Bob is level 1; Chloe is level 5; Dilbert is level 6.
* One founder online (Alice, who is also level 5).
* No legacy QORA holders.
*
* Chloe and Dilbert should receive equal shares of the 15% block reward for level 5 and 6
* Bob should receive all of the level 1 and 2 reward (5%)
* Alice should receive the remainder (80%)
*/
// We are after the shareBinFix feature trigger, so we expect level 5 and 6 to share the same reward (15%)
final int level1And2SharePercent = 5_00; // 5%
final int level5And6SharePercent = 15_00; // 10%
final long level1And2ShareAmount = (blockReward * level1And2SharePercent) / 100L / 100L;
final long level5And6ShareAmount = (blockReward * level5And6SharePercent) / 100L / 100L;
final long expectedLevel1And2Reward = level1And2ShareAmount; // The reward is given entirely to Bob
final long expectedLevel5And6Reward = level5And6ShareAmount / 2; // The reward is split between Chloe and Dilbert
final long expectedFounderReward = blockReward - level1And2ShareAmount - level5And6ShareAmount; // Alice should receive the remainder
// Validate the balances to ensure that the correct post-shareBinFix distribution is being applied
AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward);
AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance+expectedLevel1And2Reward);
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel5And6Reward);
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel5And6Reward);
}
}
/** Test rewards for level 7 and 8 accounts */
@Test
public void testLevel7And8Rewards() throws DataException {
Common.useSettings("test-settings-v2-reward-levels.json");
try (final Repository repository = RepositoryManager.getRepository()) {
List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
List<PrivateKeyAccount> mintingAndOnlineAccounts = new ArrayList<>();
// Alice self share online
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
mintingAndOnlineAccounts.add(aliceSelfShare);
// Bob self-share NOT online
// Chloe self share online
byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0);
PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey);
mintingAndOnlineAccounts.add(chloeRewardShareAccount);
// Dilbert self share online
byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0);
PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey);
mintingAndOnlineAccounts.add(dilbertRewardShareAccount);
// Mint enough blocks to bump testAccount levels to 7 and 8
final int minterBlocksNeeded = cumulativeBlocksByLevel.get(8) - 20; // 20 blocks before level 8, so that the test accounts reach the correct levels
for (int bc = 0; bc < minterBlocksNeeded; ++bc)
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
// Ensure that the levels are as we expect
assertEquals(7, (int) Common.getTestAccount(repository, "alice").getLevel());
assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel());
assertEquals(7, (int) Common.getTestAccount(repository, "chloe").getLevel());
assertEquals(8, (int) Common.getTestAccount(repository, "dilbert").getLevel());
// Now that everyone is at level 7 or 8 (except Bob who has only just started minting, so is at level 1), we can capture initial balances
Map<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT);
final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT);
final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT);
final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT);
// Mint a block
final long blockReward = BlockUtils.getNextBlockReward(repository);
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
// Ensure we are using the correct block reward value
assertEquals(100000000L, blockReward);
/*
* Alice, Chloe, and Dilbert are 'online'.
* Chloe is level 7; Dilbert is level 8.
* One founder online (Alice, who is also level 7).
* No legacy QORA holders.
*
* Chloe and Dilbert should receive equal shares of the 20% block reward for level 7 and 8
* Alice should receive the remainder (80%)
*/
// We are after the shareBinFix feature trigger, so we expect level 7 and 8 to share the same reward (20%)
final int level7And8SharePercent = 20_00; // 20%
final long level7And8ShareAmount = (blockReward * level7And8SharePercent) / 100L / 100L;
final long expectedLevel7And8Reward = level7And8ShareAmount / 2; // The reward is split between Chloe and Dilbert
final long expectedFounderReward = blockReward - level7And8ShareAmount; // Alice should receive the remainder
// Validate the balances to ensure that the correct post-shareBinFix distribution is being applied
AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward);
AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel7And8Reward);
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel7And8Reward);
}
}
/** Test rewards for level 9 and 10 accounts */
@Test
public void testLevel9And10Rewards() throws DataException {
Common.useSettings("test-settings-v2-reward-levels.json");
try (final Repository repository = RepositoryManager.getRepository()) {
List<Integer> cumulativeBlocksByLevel = BlockChain.getInstance().getCumulativeBlocksByLevel();
List<PrivateKeyAccount> mintingAndOnlineAccounts = new ArrayList<>();
// Alice self share online
PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share");
mintingAndOnlineAccounts.add(aliceSelfShare);
// Bob self-share not initially online
// Chloe self share online
byte[] chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0);
PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey);
mintingAndOnlineAccounts.add(chloeRewardShareAccount);
// Dilbert self share online
byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0);
PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey);
mintingAndOnlineAccounts.add(dilbertRewardShareAccount);
// Mint enough blocks to bump testAccount levels to 9 and 10
final int minterBlocksNeeded = cumulativeBlocksByLevel.get(10) - 20; // 20 blocks before level 10, so that the test accounts reach the correct levels
for (int bc = 0; bc < minterBlocksNeeded; ++bc)
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
// Bob self-share now comes online
byte[] bobRewardSharePrivateKey = AccountUtils.rewardShare(repository, "bob", "bob", 0);
PrivateKeyAccount bobRewardShareAccount = new PrivateKeyAccount(repository, bobRewardSharePrivateKey);
mintingAndOnlineAccounts.add(bobRewardShareAccount);
// Ensure that the levels are as we expect
assertEquals(9, (int) Common.getTestAccount(repository, "alice").getLevel());
assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel());
assertEquals(9, (int) Common.getTestAccount(repository, "chloe").getLevel());
assertEquals(10, (int) Common.getTestAccount(repository, "dilbert").getLevel());
// Now that everyone is at level 7 or 8 (except Bob who has only just started minting, so is at level 1), we can capture initial balances
Map<String, Map<Long, Long>> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA);
final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT);
final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT);
final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT);
final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT);
// Mint a block
final long blockReward = BlockUtils.getNextBlockReward(repository);
BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0]));
// Ensure we are using the correct block reward value
assertEquals(100000000L, blockReward);
/*
* Alice, Bob, Chloe, and Dilbert are 'online'.
* Bob is level 1; Chloe is level 9; Dilbert is level 10.
* One founder online (Alice, who is also level 9).
* No legacy QORA holders.
*
* Chloe and Dilbert should receive equal shares of the 25% block reward for level 9 and 10
* Bob should receive all of the level 1 and 2 reward (5%)
* Alice should receive the remainder (70%)
*/
// We are after the shareBinFix feature trigger, so we expect level 9 and 10 to share the same reward (25%)
final int level1And2SharePercent = 5_00; // 5%
final int level9And10SharePercent = 25_00; // 25%
final long level1And2ShareAmount = (blockReward * level1And2SharePercent) / 100L / 100L;
final long level9And10ShareAmount = (blockReward * level9And10SharePercent) / 100L / 100L;
final long expectedLevel1And2Reward = level1And2ShareAmount; // The reward is given entirely to Bob
final long expectedLevel9And10Reward = level9And10ShareAmount / 2; // The reward is split between Chloe and Dilbert
final long expectedFounderReward = blockReward - level1And2ShareAmount - level9And10ShareAmount; // Alice should receive the remainder
// Validate the balances to ensure that the correct post-shareBinFix distribution is being applied
AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderReward);
AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance+expectedLevel1And2Reward);
AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel9And10Reward);
AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel9And10Reward);
}
}
private int getFlags(Repository repository, String name) throws DataException {
TestAccount testAccount = Common.getTestAccount(repository, name);
return repository.getAccountRepository().getAccount(testAccount.getAddress()).getFlags();
}
}

View File

@@ -44,7 +44,11 @@
"powfixTimestamp": 0,
"qortalTimestamp": 0,
"newAssetPricingTimestamp": 0,
"groupApprovalTimestamp": 0
"groupApprovalTimestamp": 0,
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
"calcChainWeightTimestamp": 0
},
"genesisInfo": {
"version": 4,

View File

@@ -44,7 +44,11 @@
"powfixTimestamp": 0,
"qortalTimestamp": 0,
"newAssetPricingTimestamp": 0,
"groupApprovalTimestamp": 0
"groupApprovalTimestamp": 0,
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
"calcChainWeightTimestamp": 0
},
"genesisInfo": {
"version": 4,

View File

@@ -44,7 +44,11 @@
"powfixTimestamp": 0,
"qortalTimestamp": 0,
"newAssetPricingTimestamp": 0,
"groupApprovalTimestamp": 0
"groupApprovalTimestamp": 0,
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
"calcChainWeightTimestamp": 0
},
"genesisInfo": {
"version": 4,

View File

@@ -44,7 +44,11 @@
"powfixTimestamp": 0,
"qortalTimestamp": 0,
"newAssetPricingTimestamp": 0,
"groupApprovalTimestamp": 0
"groupApprovalTimestamp": 0,
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
"calcChainWeightTimestamp": 0
},
"genesisInfo": {
"version": 4,

View File

@@ -44,7 +44,11 @@
"powfixTimestamp": 0,
"qortalTimestamp": 0,
"newAssetPricingTimestamp": 0,
"groupApprovalTimestamp": 0
"groupApprovalTimestamp": 0,
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
"calcChainWeightTimestamp": 0
},
"genesisInfo": {
"version": 4,

View File

@@ -0,0 +1,75 @@
{
"isTestChain": true,
"blockTimestampMargin": 500,
"transactionExpiryPeriod": 86400000,
"maxBlockSize": 2097152,
"maxBytesPerUnitFee": 1024,
"unitFee": "0.1",
"requireGroupForApproval": false,
"minAccountLevelToRewardShare": 5,
"maxRewardSharesPerMintingAccount": 20,
"founderEffectiveMintingLevel": 10,
"onlineAccountSignaturesMinLifetime": 3600000,
"onlineAccountSignaturesMaxLifetime": 86400000,
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },
{ "height": 21, "reward": 1 }
],
"sharesByLevel": [
{ "levels": [ 1, 2 ], "share": 0.05 },
{ "levels": [ 3, 4 ], "share": 0.10 },
{ "levels": [ 5, 6 ], "share": 0.15 },
{ "levels": [ 7, 8 ], "share": 0.20 },
{ "levels": [ 9, 10 ], "share": 0.25 }
],
"qoraHoldersShare": 0.20,
"qoraPerQortReward": 250,
"blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ],
"blockTimingsByHeight": [
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
],
"ciyamAtSettings": {
"feePerStep": "0.0001",
"maxStepsPerRound": 500,
"stepsPerFunctionCall": 10,
"minutesPerBlock": 1
},
"featureTriggers": {
"messageHeight": 0,
"atHeight": 0,
"assetsTimestamp": 0,
"votingTimestamp": 0,
"arbitraryTimestamp": 0,
"powfixTimestamp": 0,
"qortalTimestamp": 0,
"newAssetPricingTimestamp": 0,
"groupApprovalTimestamp": 0,
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 6,
"calcChainWeightTimestamp": 0
},
"genesisInfo": {
"version": 4,
"timestamp": 0,
"transactions": [
{ "type": "ISSUE_ASSET", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0 },
{ "type": "ISSUE_ASSET", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
{ "type": "ISSUE_ASSET", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true },
{ "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" },
{ "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" },
{ "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" },
{ "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 },
{ "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 },
{ "type": "ACCOUNT_LEVEL", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "level": 1 },
{ "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 2 }
]
}
}

View File

@@ -44,7 +44,11 @@
"powfixTimestamp": 0,
"qortalTimestamp": 0,
"newAssetPricingTimestamp": 0,
"groupApprovalTimestamp": 0
"groupApprovalTimestamp": 0,
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
"calcChainWeightTimestamp": 0
},
"genesisInfo": {
"version": 4,

View File

@@ -44,7 +44,11 @@
"powfixTimestamp": 0,
"qortalTimestamp": 0,
"newAssetPricingTimestamp": 0,
"groupApprovalTimestamp": 0
"groupApprovalTimestamp": 0,
"atFindNextTransactionFix": 0,
"newBlockSigHeight": 999999,
"shareBinFix": 999999,
"calcChainWeightTimestamp": 0
},
"genesisInfo": {
"version": 4,

View File

@@ -1,5 +1,6 @@
{
"bitcoinNet": "REGTEST",
"litecoinNet": "REGTEST",
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2.json",
"wipeUnconfirmedOnStart": false,

View File

@@ -0,0 +1,7 @@
{
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2-reward-levels.json",
"wipeUnconfirmedOnStart": false,
"testNtpOffset": 0,
"minPeers": 0
}

View File

@@ -1,5 +1,6 @@
{
"bitcoinNet": "TEST3",
"litecoinNet": "TEST3",
"restrictedApi": false,
"blockchainConfig": "src/test/resources/test-chain-v2.json",
"wipeUnconfirmedOnStart": false,