Replaced bitcoinj networking with ElectrumX.

No more bitcoinj peer-group stalls, or slow startups,
or downloading tons of block headers, or checkpoint files.

Now we use ElectrumX protocol to query info from random servers.

Also:
BTC.hash160 callers now use Crypto.hash160 instead.
Added BitTwiddling.fromLEBytes() returns int.

Unit tests seem OK, but needs complete testnet ACCT walkthrough.
This commit is contained in:
catbref
2020-05-26 17:47:37 +01:00
parent 59de22883b
commit 3d4fc38fcb
14 changed files with 670 additions and 626 deletions

View File

@@ -2,7 +2,6 @@ package org.qortal.test.btcacct;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
@@ -11,7 +10,6 @@ import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.bitcoinj.store.BlockStoreException;
import org.bitcoinj.wallet.WalletTransaction;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -37,12 +35,10 @@ public class BtcTests extends Common {
BTC btc = BTC.getInstance();
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Long> future = executor.submit(() -> btc.getMedianBlockTime());
BTC.shutdown();
Future<Integer> future = executor.submit(() -> btc.getMedianBlockTime());
try {
Long medianBlockTime = future.get();
Integer medianBlockTime = future.get();
assertNull("Shutdown should occur before we get a result", medianBlockTime);
} catch (InterruptedException | ExecutionException e) {
}
@@ -55,12 +51,10 @@ public class BtcTests extends Common {
BTC btc = BTC.getInstance();
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Long> future = executor.submit(() -> btc.getMedianBlockTime());
BTC.shutdown();
Future<Integer> future = executor.submit(() -> btc.getMedianBlockTime());
try {
Long medianBlockTime = future.get();
Integer medianBlockTime = future.get();
assertNull("Shutdown should occur before we get a result", medianBlockTime);
} catch (InterruptedException | ExecutionException e) {
}
@@ -92,14 +86,11 @@ public class BtcTests extends Common {
public void testFindP2shSecret() {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
int startTime = 1587510000; // Tue 21 Apr 2020 23:00:00 UTC
List<WalletTransaction> walletTransactions = new ArrayList<>();
BTC.getInstance().getBalanceAndOtherInfo(p2shAddress, startTime, null, walletTransactions);
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
byte[] expectedSecret = AtTests.secret;
byte[] secret = BTCACCT.findP2shSecret(p2shAddress, walletTransactions);
byte[] secret = BTCACCT.findP2shSecret(p2shAddress, rawTransactions);
assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));

View File

@@ -14,6 +14,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
@@ -105,7 +106,7 @@ public class BuildP2SH {
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
System.out.println(String.format("P2SH address: %s", p2shAddress));

View File

@@ -16,6 +16,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
@@ -115,7 +116,7 @@ public class CheckP2SH {
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!derivedP2shAddress.equals(p2shAddress)) {
@@ -134,9 +135,7 @@ public class CheckP2SH {
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
final int startTime = lockTime - 86400;
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
@@ -144,7 +143,7 @@ public class CheckP2SH {
System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.FORMAT.format(p2shBalance)));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime);
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);

View File

@@ -0,0 +1,136 @@
package org.qortal.test.btcacct;
import static org.junit.Assert.*;
import java.security.Security;
import java.util.List;
import org.bitcoinj.core.Address;
import org.bitcoinj.params.TestNet3Params;
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.ElectrumX;
import org.qortal.utils.BitTwiddling;
import org.qortal.utils.Pair;
import com.google.common.hash.HashCode;
public class ElectrumXTests {
static {
// This must go before any calls to LogManager/Logger
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
}
@Test
public void testInstance() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
assertNotNull(electrumX);
}
@Test
public void testGetCurrentHeight() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Integer height = electrumX.getCurrentHeight();
assertNotNull(height);
assertTrue(height > 10000);
System.out.println("Current TEST3 height: " + height);
}
@Test
public void testGetRecentBlocks() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Integer height = electrumX.getCurrentHeight();
assertNotNull(height);
assertTrue(height > 10000);
List<byte[]> recentBlockHeaders = electrumX.getBlockHeaders(height - 11, 11);
assertNotNull(recentBlockHeaders);
System.out.println(String.format("Returned %d recent blocks", recentBlockHeaders.size()));
for (int i = 0; i < recentBlockHeaders.size(); ++i) {
byte[] blockHeader = recentBlockHeaders.get(i);
// Timestamp(int) is at 4 + 32 + 32 = 68 bytes offset
int offset = 4 + 32 + 32;
int timestamp = BitTwiddling.fromLEBytes(blockHeader, offset);
System.out.println(String.format("Block %d timestamp: %d", height + i, timestamp));
}
}
@Test
public void testGetP2PKHBalance() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Address address = Address.fromString(TestNet3Params.get(), "n3GNqMveyvaPvUbH469vDRadqpJMPc84JA");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
Long balance = electrumX.getBalance(script);
assertNotNull(balance);
assertTrue(balance > 0L);
System.out.println(String.format("TestNet address %s has balance: %d sats / %d.%08d BTC", address, balance, (balance / 100000000L), (balance % 100000000L)));
}
@Test
public void testGetP2SHBalance() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
Long balance = electrumX.getBalance(script);
assertNotNull(balance);
assertTrue(balance > 0L);
System.out.println(String.format("TestNet address %s has balance: %d sats / %d.%08d BTC", address, balance, (balance / 100000000L), (balance % 100000000L)));
}
@Test
public void testGetUnspentOutputs() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<Pair<byte[], Integer>> unspentOutputs = electrumX.getUnspentOutputs(script);
assertNotNull(unspentOutputs);
assertFalse(unspentOutputs.isEmpty());
for (Pair<byte[], Integer> unspentOutput : unspentOutputs)
System.out.println(String.format("TestNet address %s has unspent output at tx %s, output index %d", address, HashCode.fromBytes(unspentOutput.getA()).toString(), unspentOutput.getB()));
}
@Test
public void testGetRawTransaction() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
byte[] txHash = HashCode.fromString("7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af").asBytes();
byte[] rawTransactionBytes = electrumX.getRawTransaction(txHash);
assertNotNull(rawTransactionBytes);
}
@Test
public void testGetAddressTransactions() {
ElectrumX electrumX = ElectrumX.getInstance("TEST3");
Address address = Address.fromString(TestNet3Params.get(), "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE");
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
List<byte[]> rawTransactions = electrumX.getAddressTransactions(script);
assertNotNull(rawTransactions);
assertFalse(rawTransactions.isEmpty());
}
}

View File

@@ -22,34 +22,31 @@ public class GetTransaction {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: GetTransaction <bitcoin-tx> <start-time>"));
System.err.println(String.format("example (mainnet): GetTransaction 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660 1585317000"));
System.err.println(String.format("example (testnet): GetTransaction 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e 1584376000"));
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.exit(1);
}
public static void main(String[] args) {
if (args.length < 2 || args.length > 2)
if (args.length != 1)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Settings.fileInstance("settings-test.json");
byte[] transactionId = null;
int startTime = 0;
try {
int argIndex = 0;
transactionId = HashCode.fromString(args[argIndex++]).asBytes();
startTime = Integer.parseInt(args[argIndex++]);
} catch (NumberFormatException | AddressFormatException e) {
usage(String.format("Argument format exception: %s", e.getMessage()));
}
// Grab all outputs from transaction
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(transactionId, startTime);
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(transactionId);
if (fundingOutputs == null) {
System.out.println(String.format("Transaction not found"));
return;

View File

@@ -19,6 +19,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
@@ -111,7 +112,7 @@ public class Redeem {
// New/derived info
byte[] secretHash = BTC.hash160(secret);
byte[] secretHash = Crypto.hash160(secret);
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey);
@@ -123,7 +124,7 @@ public class Redeem {
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!derivedP2shAddress.equals(p2shAddress)) {
@@ -146,9 +147,7 @@ public class Redeem {
}
// Check P2SH is funded
final int startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400;
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
@@ -156,7 +155,7 @@ public class Redeem {
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime);
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);

View File

@@ -19,6 +19,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qortal.controller.Controller;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryFactory;
@@ -122,7 +123,7 @@ public class Refund {
byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!derivedP2shAddress.equals(p2shAddress)) {
@@ -150,9 +151,7 @@ public class Refund {
}
// Check P2SH is funded
final int startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400;
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null) {
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
System.exit(2);
@@ -160,7 +159,7 @@ public class Refund {
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
// Grab all P2SH funding transactions (just in case there are more than one)
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime);
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);