mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-23 04:36:50 +00:00
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:
@@ -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));
|
||||
|
@@ -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));
|
||||
|
@@ -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);
|
||||
|
136
src/test/java/org/qortal/test/btcacct/ElectrumXTests.java
Normal file
136
src/test/java/org/qortal/test/btcacct/ElectrumXTests.java
Normal 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());
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
Reference in New Issue
Block a user