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

@ -30,7 +30,6 @@ import org.bitcoinj.core.LegacyAddress;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.script.Script.ScriptType;
import org.bitcoinj.wallet.WalletTransaction;
import org.qortal.account.PublicKeyAccount;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
@ -452,7 +451,7 @@ public class CrossChainResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), crossChainTradeData.lockTime, redeemBitcoinAddress.getHash(), crossChainTradeData.secretHash);
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
return p2shAddress.toString();
@ -522,22 +521,19 @@ public class CrossChainResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), crossChainTradeData.lockTime, redeemBitcoinAddress.getHash(), crossChainTradeData.secretHash);
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
Long medianBlockTime = BTC.getInstance().getMedianBlockTime();
Integer medianBlockTime = BTC.getInstance().getMedianBlockTime();
if (medianBlockTime == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
long now = NTP.getTime();
// Check P2SH is funded
final int startTime = (int) (crossChainTradeData.tradeModeTimestamp / 1000L);
List<TransactionOutput> fundingOutputs = new ArrayList<>();
List<WalletTransaction> walletTransactions = new ArrayList<>();
Coin p2shBalance = BTC.getInstance().getBalanceAndOtherInfo(p2shAddress.toString(), startTime, fundingOutputs, walletTransactions);
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
@ -545,6 +541,8 @@ public class CrossChainResource {
p2shStatus.bitcoinP2shAddress = p2shAddress.toString();
p2shStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance.value, 8);
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
if (p2shBalance.value >= crossChainTradeData.expectedBitcoin && fundingOutputs.size() == 1) {
p2shStatus.canRedeem = now >= medianBlockTime * 1000L;
p2shStatus.canRefund = now >= crossChainTradeData.lockTime * 1000L;
@ -552,7 +550,8 @@ public class CrossChainResource {
if (now >= medianBlockTime * 1000L) {
// See if we can extract secret
p2shStatus.secret = BTCACCT.findP2shSecret(p2shStatus.bitcoinP2shAddress, walletTransactions);
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shStatus.bitcoinP2shAddress);
p2shStatus.secret = BTCACCT.findP2shSecret(p2shStatus.bitcoinP2shAddress, rawTransactions);
}
return p2shStatus;
@ -630,20 +629,19 @@ public class CrossChainResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), crossChainTradeData.lockTime, redeemBitcoinAddress.getHash(), crossChainTradeData.secretHash);
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
long now = NTP.getTime();
// Check P2SH is funded
final int startTime = (int) (crossChainTradeData.tradeModeTimestamp / 1000L);
List<TransactionOutput> fundingOutputs = new ArrayList<>();
Coin p2shBalance = BTC.getInstance().getBalanceAndOtherInfo(p2shAddress.toString(), startTime, fundingOutputs, null);
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
if (fundingOutputs.size() != 1)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
@ -741,24 +739,22 @@ public class CrossChainResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), crossChainTradeData.lockTime, redeemAddress.getHash(), crossChainTradeData.secretHash);
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
Long medianBlockTime = BTC.getInstance().getMedianBlockTime();
Integer medianBlockTime = BTC.getInstance().getMedianBlockTime();
if (medianBlockTime == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
long now = NTP.getTime();
// Check P2SH is funded
final int startTime = (int) (crossChainTradeData.tradeModeTimestamp / 1000L);
List<TransactionOutput> fundingOutputs = new ArrayList<>();
Coin p2shBalance = BTC.getInstance().getBalanceAndOtherInfo(p2shAddress.toString(), startTime, fundingOutputs, null);
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
if (p2shBalance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
if (fundingOutputs.size() != 1)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);

View File

@ -35,7 +35,6 @@ import org.qortal.block.BlockChain;
import org.qortal.block.BlockMinter;
import org.qortal.block.BlockChain.BlockTimingByHeight;
import org.qortal.controller.Synchronizer.SynchronizationResult;
import org.qortal.crosschain.BTC;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.MintingAccountData;
import org.qortal.data.account.RewardShareData;
@ -676,9 +675,6 @@ public class Controller extends Thread {
if (!isStopping) {
isStopping = true;
LOGGER.info("Shutting down Bitcoin support");
BTC.shutdown();
LOGGER.info("Shutting down API");
ApiService.getInstance().stop();

View File

@ -1,68 +1,24 @@
package org.qortal.crosschain;
import java.io.ByteArrayInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.TreeMap;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.CheckpointManager;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Peer;
import org.bitcoinj.core.PeerAddress;
import org.bitcoinj.core.PeerGroup;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.StoredBlock;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionBroadcast;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.listeners.BlocksDownloadedEventListener;
import org.bitcoinj.core.listeners.NewBestBlockListener;
import org.bitcoinj.net.discovery.DnsDiscovery;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script.ScriptType;
import org.bitcoinj.store.BlockStore;
import org.bitcoinj.store.BlockStoreException;
import org.bitcoinj.store.MemoryBlockStore;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.utils.MonetaryFormat;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.WalletTransaction;
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
import org.bitcoinj.wallet.listeners.WalletCoinsSentEventListener;
import org.qortal.settings.Settings;
import org.qortal.utils.BitTwiddling;
import org.qortal.utils.Pair;
public class BTC {
@ -71,19 +27,10 @@ public class BTC {
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
public static final int HASH160_LENGTH = 20;
private static final MessageDigest RIPE_MD160_DIGESTER;
private static final MessageDigest SHA256_DIGESTER;
static {
try {
RIPE_MD160_DIGESTER = MessageDigest.getInstance("RIPEMD160");
SHA256_DIGESTER = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
public enum BitcoinNet {
MAIN {
@Override
@ -107,156 +54,9 @@ public class BTC {
public abstract NetworkParameters getParams();
}
private static class UpdateableCheckpointManager extends CheckpointManager implements NewBestBlockListener {
private static final long CHECKPOINT_THRESHOLD = 7 * 24 * 60 * 60; // seconds
private static final String MINIMAL_TESTNET3_TEXTFILE = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO\n";
private static final String MINIMAL_MAINNET_TEXTFILE = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAABjl7tqvU/FIcDT9gcbVlA4nwtFUbxAtOawZzBpAAAAAKzkcK7NqciBjI/ldojNKncrWleVSgDfBCCn3VRrbSxXaw5/Sf//AB0z8Bkv\n";
public UpdateableCheckpointManager(NetworkParameters params, File checkpointsFile) throws IOException, InterruptedException {
super(params, getMinimalTextFileStream(params, checkpointsFile));
}
public UpdateableCheckpointManager(NetworkParameters params, InputStream inputStream) throws IOException {
super(params, inputStream);
}
private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params, File checkpointsFile) throws IOException, InterruptedException {
if (params == MainNetParams.get())
return new ByteArrayInputStream(MINIMAL_MAINNET_TEXTFILE.getBytes());
if (params == TestNet3Params.get())
return new ByteArrayInputStream(MINIMAL_TESTNET3_TEXTFILE.getBytes());
if (params == RegTestParams.get())
return newRegTestCheckpointsStream(checkpointsFile); // We have to build this
throw new FileNotFoundException("Failed to construct empty UpdateableCheckpointManageer");
}
private static ByteArrayInputStream newRegTestCheckpointsStream(File checkpointsFile) throws IOException, InterruptedException {
try {
final NetworkParameters params = RegTestParams.get();
final BlockStore store = new MemoryBlockStore(params);
final BlockChain chain = new BlockChain(params, store);
final PeerGroup peerGroup = new PeerGroup(params, chain);
final InetAddress ipAddress = InetAddress.getLoopbackAddress();
final PeerAddress peerAddress = new PeerAddress(params, ipAddress);
peerGroup.addAddress(peerAddress);
// startAsync().get() to allow interruption
peerGroup.startAsync().get();
final TreeMap<Integer, StoredBlock> checkpoints = new TreeMap<>();
chain.addNewBestBlockListener((block) -> checkpoints.put(block.getHeight(), block));
peerGroup.downloadBlockChain();
peerGroup.stop();
saveAsText(checkpointsFile, checkpoints.values());
return new ByteArrayInputStream(Files.readAllBytes(checkpointsFile.toPath()));
} catch (BlockStoreException e) {
throw new IOException(e);
} catch (ExecutionException e) {
// Couldn't start peerGroup
throw new IOException(e);
}
}
@Override
public void notifyNewBestBlock(StoredBlock block) {
final int height = block.getHeight();
if (height % this.params.getInterval() != 0)
return;
final long blockTimestamp = block.getHeader().getTimeSeconds();
final long now = System.currentTimeMillis() / 1000L;
if (blockTimestamp > now - CHECKPOINT_THRESHOLD)
return; // Too recent
LOGGER.trace(() -> String.format("Checkpointing at block %d dated %s", height, LocalDateTime.ofInstant(Instant.ofEpochSecond(blockTimestamp), ZoneOffset.UTC)));
this.checkpoints.put(blockTimestamp, block);
try {
saveAsText(new File(BTC.getInstance().getDirectory(), BTC.getInstance().getCheckpointsFileName()), this.checkpoints.values());
} catch (FileNotFoundException e) {
// Save failed - log it but it's not critical
LOGGER.warn(() -> String.format("Failed to save updated BTC checkpoints: %s", e.getMessage()));
}
}
private static void saveAsText(File textFile, Collection<StoredBlock> checkpointBlocks) throws FileNotFoundException {
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(textFile), StandardCharsets.US_ASCII))) {
writer.println("TXT CHECKPOINTS 1");
writer.println("0"); // Number of signatures to read. Do this later.
writer.println(checkpointBlocks.size());
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
for (StoredBlock block : checkpointBlocks) {
block.serializeCompact(buffer);
writer.println(CheckpointManager.BASE64.encode(buffer.array()));
buffer.position(0);
}
}
}
@SuppressWarnings("unused")
public void saveAsBinary(File file) throws IOException {
try (final FileOutputStream fileOutputStream = new FileOutputStream(file, false)) {
MessageDigest digest = Sha256Hash.newDigest();
try (final DigestOutputStream digestOutputStream = new DigestOutputStream(fileOutputStream, digest)) {
digestOutputStream.on(false);
try (final DataOutputStream dataOutputStream = new DataOutputStream(digestOutputStream)) {
dataOutputStream.writeBytes("CHECKPOINTS 1");
dataOutputStream.writeInt(0); // Number of signatures to read. Do this later.
digestOutputStream.on(true);
dataOutputStream.writeInt(this.checkpoints.size());
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
for (StoredBlock block : this.checkpoints.values()) {
block.serializeCompact(buffer);
dataOutputStream.write(buffer.array());
buffer.position(0);
}
}
}
}
}
}
private static class ResettableBlockChain extends BlockChain {
public ResettableBlockChain(NetworkParameters params, BlockStore blockStore) throws BlockStoreException {
super(params, blockStore);
}
// Overridden to increase visibility to public
@Override
public void setChainHead(StoredBlock chainHead) throws BlockStoreException {
super.setChainHead(chainHead);
}
}
private static final Object instanceLock = new Object();
private static BTC instance;
private enum RunningState { RUNNING, STOPPED };
FutureTask<RunningState> startupFuture;
private final NetworkParameters params;
private final String checkpointsFileName;
private final File directory;
private PeerGroup peerGroup;
private BlockStore blockStore;
private ResettableBlockChain chain;
private UpdateableCheckpointManager manager;
private final ElectrumX electrumX;
// Constructors and instance
@ -264,399 +64,96 @@ public class BTC {
BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet();
this.params = bitcoinNet.getParams();
switch (bitcoinNet) {
case MAIN:
this.checkpointsFileName = "checkpoints.txt";
break;
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", bitcoinNet.name()));
case TEST3:
this.checkpointsFileName = "checkpoints-testnet.txt";
break;
case REGTEST:
this.checkpointsFileName = "checkpoints-regtest.txt";
break;
default:
throw new IllegalStateException("Unsupported Bitcoin network: " + bitcoinNet.name());
}
this.directory = new File("Qortal-BTC");
startupFuture = new FutureTask<>(BTC::startUp);
this.electrumX = ElectrumX.getInstance(bitcoinNet.name());
}
public static BTC getInstance() {
synchronized (instanceLock) {
if (instance == null) {
instance = new BTC();
Executors.newSingleThreadExecutor().execute(instance.startupFuture);
}
public static synchronized BTC getInstance() {
if (instance == null)
instance = new BTC();
return instance;
}
return instance;
}
// Getters & setters
/* package */ File getDirectory() {
return this.directory;
}
/* package */ String getCheckpointsFileName() {
return this.checkpointsFileName;
}
public NetworkParameters getNetworkParameters() {
return this.params;
}
// Static utility methods
public static byte[] hash160(byte[] message) {
return RIPE_MD160_DIGESTER.digest(SHA256_DIGESTER.digest(message));
}
// Start-up & shutdown
private static RunningState startUp() {
Thread.currentThread().setName("Bitcoin support");
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", Settings.getInstance().getBitcoinNet().name()));
final long startTime = System.currentTimeMillis() / 1000L;
if (!instance.directory.exists())
if (!instance.directory.mkdirs()) {
LOGGER.error(() -> String.format("Stopping Bitcoin support: couldn't create directory '%s'", instance.directory.getName()));
return RunningState.STOPPED;
}
File checkpointsFile = new File(instance.directory, instance.checkpointsFileName);
try (InputStream checkpointsStream = new FileInputStream(checkpointsFile)) {
instance.manager = new UpdateableCheckpointManager(instance.params, checkpointsStream);
} catch (FileNotFoundException e) {
// Construct with no checkpoints then
try {
instance.manager = new UpdateableCheckpointManager(instance.params, checkpointsFile);
} catch (IOException e2) {
LOGGER.error(() -> String.format("Stopping Bitcoin support: couldn't create checkpoints file: %s", e.getMessage()));
return RunningState.STOPPED;
} catch (InterruptedException e2) {
// Probably normal shutdown so quietly return
LOGGER.debug("Stopping Bitcoin support due to interrupt");
return RunningState.STOPPED;
}
} catch (IOException e) {
LOGGER.error(() -> String.format("Stopping Bitcoin support: couldn't load checkpoints file: %s", e.getMessage()));
return RunningState.STOPPED;
}
try {
StoredBlock checkpoint = instance.manager.getCheckpointBefore(startTime - 1);
instance.blockStore = new MemoryBlockStore(instance.params);
instance.blockStore.put(checkpoint);
instance.blockStore.setChainHead(checkpoint);
instance.chain = new ResettableBlockChain(instance.params, instance.blockStore);
} catch (BlockStoreException e) {
LOGGER.error(() -> String.format("Stopping Bitcoin support: couldn't initialize blockstore: %s", e.getMessage()));
return RunningState.STOPPED;
}
instance.peerGroup = new PeerGroup(instance.params, instance.chain);
instance.peerGroup.setUserAgent("qortal", "1.0");
// instance.peerGroup.setPingIntervalMsec(1000L);
// instance.peerGroup.setMaxConnections(20);
if (instance.params != RegTestParams.get()) {
instance.peerGroup.addPeerDiscovery(new DnsDiscovery(instance.params));
} else {
instance.peerGroup.addAddress(PeerAddress.localhost(instance.params));
}
// final check that we haven't been interrupted
if (Thread.currentThread().isInterrupted()) {
LOGGER.debug("Stopping Bitcoin support due to interrupt");
return RunningState.STOPPED;
}
// startAsync() so we can return
instance.peerGroup.startAsync();
return RunningState.RUNNING;
}
public static void shutdown() {
// This is make sure we don't check instance
// while some other thread is in the middle of BTC.getInstance()
synchronized (instanceLock) {
if (instance == null)
return;
}
// If we can't cancel because we've finished start-up with RUNNING state then stop peerGroup.
// Has side-effect of cancelling in-progress start-up, which is what we want too.
try {
if (!instance.startupFuture.cancel(true)
&& instance.startupFuture.isDone()
&& instance.startupFuture.get() == RunningState.RUNNING)
instance.peerGroup.stop();
} catch (InterruptedException | ExecutionException | CancellationException e) {
// Start-up was in-progress when cancel() called, so this is ok
}
}
public static void resetForTesting() {
synchronized (instanceLock) {
instance = null;
}
}
// Utility methods
/** Returns whether Bitcoin support is running, blocks until RUNNING if STARTING. */
private boolean isRunning() {
try {
return this.startupFuture.get() == RunningState.RUNNING;
} catch (InterruptedException | ExecutionException | CancellationException e) {
return false;
}
}
protected Wallet createEmptyWallet() {
return Wallet.createBasic(this.params);
}
private class ReplayHooks {
private Runnable preReplay;
private Runnable postReplay;
public ReplayHooks(Runnable preReplay, Runnable postReplay) {
this.preReplay = preReplay;
this.postReplay = postReplay;
}
public void preReplay() {
this.preReplay.run();
}
public void postReplay() {
this.postReplay.run();
}
}
private void replayChain(int startTime, Wallet wallet, ReplayHooks replayHooks) throws BlockStoreException {
StoredBlock checkpoint = this.manager.getCheckpointBefore(startTime - 1);
this.blockStore.put(checkpoint);
this.blockStore.setChainHead(checkpoint);
this.chain.setChainHead(checkpoint);
final WalletCoinsReceivedEventListener coinsReceivedListener = (someWallet, tx, prevBalance, newBalance) -> {
LOGGER.trace(() -> String.format("Wallet-related transaction %s", tx.getTxId()));
};
final WalletCoinsSentEventListener coinsSentListener = (someWallet, tx, prevBalance, newBalance) -> {
LOGGER.trace(() -> String.format("Wallet-related transaction %s", tx.getTxId()));
};
if (wallet != null) {
wallet.addCoinsReceivedEventListener(coinsReceivedListener);
wallet.addCoinsSentEventListener(coinsSentListener);
// Link wallet to chain and peerGroup
this.chain.addWallet(wallet);
this.peerGroup.addWallet(wallet);
}
try {
if (replayHooks != null)
replayHooks.preReplay();
// Sync blockchain using peerGroup, skipping as much as we can before startTime
this.peerGroup.setFastCatchupTimeSecs(startTime);
this.chain.addNewBestBlockListener(Threading.SAME_THREAD, this.manager);
this.peerGroup.downloadBlockChain();
} finally {
// Clean up
if (replayHooks != null)
replayHooks.postReplay();
if (wallet != null) {
wallet.removeCoinsReceivedEventListener(coinsReceivedListener);
wallet.removeCoinsSentEventListener(coinsSentListener);
this.peerGroup.removeWallet(wallet);
this.chain.removeWallet(wallet);
}
// For safety, disconnect download peer just in case
Peer downloadPeer = this.peerGroup.getDownloadPeer();
if (downloadPeer != null)
downloadPeer.close();
}
public static synchronized void resetForTesting() {
instance = null;
}
// Actual useful methods for use by other classes
/** Returns median timestamp from latest 11 blocks, in seconds. */
public Long getMedianBlockTime() {
if (!this.isRunning())
// Failed to start up, or we're shutting down
public Integer getMedianBlockTime() {
Integer height = this.electrumX.getCurrentHeight();
if (height == null)
return null;
// 11 blocks, at roughly 10 minutes per block, means we should go back at least 110 minutes
// but some blocks have been way longer than 10 minutes, so be massively pessimistic
int startTime = (int) (System.currentTimeMillis() / 1000L) - 11 * 60 * 60; // 11 hours before now, in seconds
try {
this.replayChain(startTime, null, null);
List<StoredBlock> latestBlocks = new ArrayList<>(11);
StoredBlock block = this.blockStore.getChainHead();
for (int i = 0; i < 11; ++i) {
latestBlocks.add(block);
block = block.getPrev(this.blockStore);
// If previous block is null then chain replay didn't fetch any recent blocks (REGTEST)
if (block == null)
return null;
}
// Descending, but order shouldn't matter as we're picking median...
latestBlocks.sort((a, b) -> Long.compare(b.getHeader().getTimeSeconds(), a.getHeader().getTimeSeconds()));
return latestBlocks.get(5).getHeader().getTimeSeconds();
} catch (BlockStoreException e) {
LOGGER.error(String.format("Can't get Bitcoin median block time due to blockstore issue: %s", e.getMessage()));
// Grab latest 11 blocks
List<byte[]> blockHeaders = this.electrumX.getBlockHeaders(height, 11);
if (blockHeaders == null || blockHeaders.size() < 11)
return null;
}
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.fromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
// Descending, but order shouldn't matter as we're picking median...
blockTimestamps.sort((a, b) -> Integer.compare(b, a));
return blockTimestamps.get(5);
}
public Coin getBalance(String base58Address, int startTime) {
if (!this.isRunning())
// Failed to start up, or we're shutting down
public Coin getBalance(String base58Address) {
Long balance = this.electrumX.getBalance(addressToScript(base58Address));
if (balance == null)
return null;
// Create new wallet containing only the address we're interested in, ignoring anything prior to startTime
Wallet wallet = createEmptyWallet();
Address address = Address.fromString(this.params, base58Address);
wallet.addWatchedAddress(address, startTime);
try {
replayChain(startTime, wallet, null);
// Now that blockchain is up-to-date, return current balance
return wallet.getBalance();
} catch (BlockStoreException e) {
LOGGER.error(String.format("Can't get Bitcoin balance for %s due to blockstore issue: %s", base58Address, e.getMessage()));
return null;
}
return Coin.valueOf(balance);
}
public List<TransactionOutput> getOutputs(String base58Address, int startTime) {
if (!this.isRunning())
// Failed to start up, or we're shutting down
public List<TransactionOutput> getUnspentOutputs(String base58Address) {
List<Pair<byte[], Integer>> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address));
if (unspentOutputs == null)
return null;
Wallet wallet = createEmptyWallet();
Address address = Address.fromString(this.params, base58Address);
wallet.addWatchedAddress(address, startTime);
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
for (Pair<byte[], Integer> unspentOutput : unspentOutputs) {
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.getA());
if (transactionOutputs == null)
return null;
try {
replayChain(startTime, wallet, null);
// Now that blockchain is up-to-date, return outputs
return wallet.getWatchedOutputs(true);
} catch (BlockStoreException e) {
LOGGER.error(String.format("Can't get Bitcoin outputs for %s due to blockstore issue: %s", base58Address, e.getMessage()));
return null;
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.getB()));
}
return unspentTransactionOutputs;
}
public Coin getBalanceAndOtherInfo(String base58Address, int startTime, List<TransactionOutput> unspentOutputs, List<WalletTransaction> walletTransactions) {
if (!this.isRunning())
// Failed to start up, or we're shutting down
public List<TransactionOutput> getOutputs(byte[] txHash) {
byte[] rawTransactionBytes = this.electrumX.getRawTransaction(txHash);
if (rawTransactionBytes == null)
return null;
// Create new wallet containing only the address we're interested in, ignoring anything prior to startTime
Wallet wallet = createEmptyWallet();
Address address = Address.fromString(this.params, base58Address);
wallet.addWatchedAddress(address, startTime);
try {
replayChain(startTime, wallet, null);
if (unspentOutputs != null)
unspentOutputs.addAll(wallet.getWatchedOutputs(true));
if (walletTransactions != null)
for (WalletTransaction walletTransaction : wallet.getWalletTransactions())
walletTransactions.add(walletTransaction);
return wallet.getBalance();
} catch (BlockStoreException e) {
LOGGER.error(String.format("Can't get Bitcoin info for %s due to blockstore issue: %s", base58Address, e.getMessage()));
return null;
}
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
return transaction.getOutputs();
}
public List<TransactionOutput> getOutputs(byte[] txId, int startTime) {
if (!this.isRunning())
// Failed to start up, or we're shutting down
return null;
Wallet wallet = createEmptyWallet();
// Add random address to wallet
ECKey fakeKey = new ECKey();
wallet.addWatchedAddress(Address.fromKey(this.params, fakeKey, ScriptType.P2PKH), startTime);
final Sha256Hash txHash = Sha256Hash.wrap(txId);
final AtomicReference<Transaction> foundTransaction = new AtomicReference<>();
final BlocksDownloadedEventListener listener = (peer, block, filteredBlock, blocksLeft) -> {
List<Transaction> transactions = block.getTransactions();
if (transactions == null)
return;
for (Transaction transaction : transactions)
if (transaction.getTxId().equals(txHash)) {
LOGGER.trace(() -> String.format("We downloaded block containing tx %s", txHash));
foundTransaction.set(transaction);
}
};
ReplayHooks replayHooks = new ReplayHooks(() -> this.peerGroup.addBlocksDownloadedEventListener(listener), () -> this.peerGroup.removeBlocksDownloadedEventListener(listener));
// Replay chain in the hope it will download transactionId as a dependency
try {
replayChain(startTime, wallet, replayHooks);
Transaction realTx = foundTransaction.get();
return realTx.getOutputs();
} catch (BlockStoreException e) {
LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage()));
return null;
}
public List<byte[]> getAddressTransactions(String base58Address) {
return this.electrumX.getAddressTransactions(addressToScript(base58Address));
}
public boolean broadcastTransaction(Transaction transaction) {
if (!this.isRunning())
// Failed to start up, or we're shutting down
return false;
return this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
}
TransactionBroadcast transactionBroadcast = this.peerGroup.broadcastTransaction(transaction);
// Utility methods for us
try {
transactionBroadcast.future().get();
return true;
} catch (InterruptedException | ExecutionException e) {
return false;
}
private byte[] addressToScript(String base58Address) {
Address address = Address.fromString(this.params, base58Address);
return ScriptBuilder.createOutputScript(address).getProgram();
}
}

View File

@ -21,7 +21,6 @@ import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptChunk;
import org.bitcoinj.script.ScriptOpCodes;
import org.bitcoinj.script.Script.ScriptType;
import org.bitcoinj.wallet.WalletTransaction;
import org.ciyam.at.API;
import org.ciyam.at.CompilationException;
import org.ciyam.at.FunctionCode;
@ -619,11 +618,11 @@ public class BTCACCT {
return tradeData;
}
public static byte[] findP2shSecret(String p2shAddress, List<WalletTransaction> walletTransactions) {
public static byte[] findP2shSecret(String p2shAddress, List<byte[]> rawTransactions) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
for (WalletTransaction walletTransaction : walletTransactions) {
Transaction transaction = walletTransaction.getTransaction();
for (byte[] rawTransaction : rawTransactions) {
Transaction transaction = new Transaction(params, rawTransaction);
// Cycle through inputs, looking for one that spends our P2SH
for (TransactionInput input : transaction.getInputs()) {

View File

@ -0,0 +1,387 @@
package org.qortal.crosschain;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.Scanner;
import java.util.Set;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.qortal.crypto.Crypto;
import org.qortal.crypto.TrustlessSSLSocketFactory;
import org.qortal.utils.Pair;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
public class ElectrumX {
private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class);
private static final Random RANDOM = new Random();
private static final int DEFAULT_TCP_PORT = 50001;
private static final int DEFAULT_SSL_PORT = 50002;
private static final int BLOCK_HEADER_LENGTH = 80;
private static final Map<String, ElectrumX> instances = new HashMap<>();
static class Server {
String hostname;
enum ConnectionType { TCP, SSL };
ConnectionType connectionType;
int port;
public Server(String hostname, ConnectionType connectionType, int port) {
this.hostname = hostname;
this.connectionType = connectionType;
this.port = port;
}
@Override
public boolean equals(Object other) {
if (other == this)
return true;
if (!(other instanceof Server))
return false;
Server otherServer = (Server) other;
return this.connectionType == otherServer.connectionType
&& this.port == otherServer.port
&& this.hostname.equals(otherServer.hostname);
}
@Override
public int hashCode() {
return this.hostname.hashCode() ^ this.port;
}
@Override
public String toString() {
return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port);
}
}
private Set<Server> servers = new HashSet<>();
private Server currentServer;
private Socket socket;
private Scanner scanner;
private int nextId = 1;
// Constructors
private ElectrumX(String bitcoinNetwork) {
switch (bitcoinNetwork) {
case "MAIN":
servers.addAll(Arrays.asList());
break;
case "TEST3":
servers.addAll(Arrays.asList(
new Server("tn.not.fyi", Server.ConnectionType.TCP, 55001),
new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002),
new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002),
new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012)));
break;
case "REGTEST":
servers.addAll(Arrays.asList(
new Server("localhost", Server.ConnectionType.TCP, DEFAULT_TCP_PORT),
new Server("localhost", Server.ConnectionType.SSL, DEFAULT_SSL_PORT)));
break;
default:
throw new IllegalArgumentException(String.format("Bitcoin network '%s' unknown", bitcoinNetwork));
}
LOGGER.debug(() -> String.format("Starting ElectrumX support for %s Bitcoin network", bitcoinNetwork));
rpc("server.banner");
}
public static synchronized ElectrumX getInstance(String bitcoinNetwork) {
if (!instances.containsKey(bitcoinNetwork))
instances.put(bitcoinNetwork, new ElectrumX(bitcoinNetwork));
return instances.get(bitcoinNetwork);
}
// Methods for use by other classes
public Integer getCurrentHeight() {
JSONObject blockJson = (JSONObject) this.rpc("blockchain.headers.subscribe");
if (blockJson == null || !blockJson.containsKey("height"))
return null;
return ((Long) blockJson.get("height")).intValue();
}
public List<byte[]> getBlockHeaders(int startHeight, long count) {
JSONObject blockJson = (JSONObject) this.rpc("blockchain.block.headers", startHeight, count);
if (blockJson == null || !blockJson.containsKey("count") || !blockJson.containsKey("hex"))
return null;
Long returnedCount = (Long) blockJson.get("count");
String hex = (String) blockJson.get("hex");
byte[] raw = HashCode.fromString(hex).asBytes();
if (raw.length != returnedCount * BLOCK_HEADER_LENGTH)
return null;
List<byte[]> rawBlockHeaders = new ArrayList<>(returnedCount.intValue());
for (int i = 0; i < returnedCount; ++i)
rawBlockHeaders.add(Arrays.copyOfRange(raw, i * BLOCK_HEADER_LENGTH, (i + 1) * BLOCK_HEADER_LENGTH));
return rawBlockHeaders;
}
public Long getBalance(byte[] script) {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
JSONObject balanceJson = (JSONObject) this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString());
if (balanceJson == null || !balanceJson.containsKey("confirmed"))
return null;
return (Long) balanceJson.get("confirmed");
}
public List<Pair<byte[], Integer>> getUnspentOutputs(byte[] script) {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
JSONArray unspentJson = (JSONArray) this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString());
if (unspentJson == null)
return null;
List<Pair<byte[], Integer>> unspentOutputs = new ArrayList<>();
for (Object rawUnspent : unspentJson) {
JSONObject unspent = (JSONObject) rawUnspent;
byte[] txHash = HashCode.fromString((String) unspent.get("tx_hash")).asBytes();
int outputIndex = ((Long) unspent.get("tx_pos")).intValue();
unspentOutputs.add(new Pair<>(txHash, outputIndex));
}
return unspentOutputs;
}
public byte[] getRawTransaction(byte[] txHash) {
String rawTransactionHex = (String) this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString());
if (rawTransactionHex == null)
return null;
return HashCode.fromString(rawTransactionHex).asBytes();
}
public List<byte[]> getAddressTransactions(byte[] script) {
byte[] scriptHash = Crypto.digest(script);
Bytes.reverse(scriptHash);
JSONArray transactionsJson = (JSONArray) this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString());
if (transactionsJson == null)
return null;
List<byte[]> rawTransactions = new ArrayList<>();
for (Object rawTransactionInfo : transactionsJson) {
JSONObject transactionInfo = (JSONObject) rawTransactionInfo;
// We only want confirmed transactions
if (!transactionInfo.containsKey("height"))
continue;
String txHash = (String) transactionInfo.get("tx_hash");
String rawTransactionHex = (String) this.rpc("blockchain.transaction.get", txHash);
if (rawTransactionHex == null)
return null;
rawTransactions.add(HashCode.fromString(rawTransactionHex).asBytes());
}
return rawTransactions;
}
public boolean broadcastTransaction(byte[] transactionBytes) {
JSONObject broadcastJson = (JSONObject) this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString());
if (broadcastJson == null)
return false;
// If JSON contains "result", then it went through ok.
// Otherwise JSON would contain "error" instead.
return broadcastJson.containsKey("result");
}
// Class-private utility methods
private Set<Server> serverPeersSubscribe() {
Set<Server> newServers = new HashSet<>();
JSONArray peers = (JSONArray) this.connectedRpc("server.peers.subscribe");
if (peers == null)
return newServers;
for (Object rawPeer : peers) {
JSONArray peer = (JSONArray) rawPeer;
if (peer.size() < 3)
continue;
String hostname = (String) peer.get(1);
JSONArray features = (JSONArray) peer.get(2);
for (Object rawFeature : features) {
String feature = (String) rawFeature;
Server.ConnectionType connectionType = null;
int port = -1;
switch (feature.charAt(0)) {
case 's':
connectionType = Server.ConnectionType.SSL;
port = DEFAULT_SSL_PORT;
break;
case 't':
connectionType = Server.ConnectionType.TCP;
port = DEFAULT_TCP_PORT;
break;
}
if (connectionType == null)
continue;
// Possible non-default port?
if (feature.length() > 1)
try {
port = Integer.parseInt(feature.substring(1));
} catch (NumberFormatException e) {
// no good
continue; // for-loop above
}
Server newServer = new Server(hostname, connectionType, port);
newServers.add(newServer);
}
}
return newServers;
}
private synchronized Object rpc(String method, Object...params) {
while (haveConnection()) {
Object response = connectedRpc(method, params);
if (response != null)
return response;
this.currentServer = null;
try {
this.socket.close();
} catch (IOException e) {
/* ignore */
}
this.scanner = null;
}
return null;
}
private boolean haveConnection() {
if (this.currentServer != null)
return true;
List<Server> remainingServers = new ArrayList<>(this.servers);
while (!remainingServers.isEmpty()) {
Server server = remainingServers.remove(RANDOM.nextInt(remainingServers.size()));
LOGGER.trace(() -> String.format("Connecting to %s", server));
try {
SocketAddress endpoint = new InetSocketAddress(server.hostname, server.port);
int timeout = 5000; // ms
this.socket = new Socket();
this.socket.connect(endpoint, timeout);
this.socket.setTcpNoDelay(true);
if (server.connectionType == Server.ConnectionType.SSL) {
SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory();
this.socket = (SSLSocket) factory.createSocket(this.socket, server.hostname, server.port, true);
}
this.scanner = new Scanner(this.socket.getInputStream());
this.scanner.useDelimiter("\n");
// Check connection works by asking for more servers
Set<Server> moreServers = serverPeersSubscribe();
moreServers.removeAll(this.servers);
remainingServers.addAll(moreServers);
this.servers.addAll(moreServers);
LOGGER.debug(() -> String.format("Connected to %s", server));
this.currentServer = server;
return true;
} catch (IOException e) {
// Try another server...
this.socket = null;
this.scanner = null;
}
}
return false;
}
@SuppressWarnings("unchecked")
private Object connectedRpc(String method, Object...params) {
JSONObject requestJson = new JSONObject();
requestJson.put("id", this.nextId++);
requestJson.put("method", method);
JSONArray requestParams = new JSONArray();
requestParams.addAll(Arrays.asList(params));
requestJson.put("params", requestParams);
String request = requestJson.toJSONString() + "\n";
LOGGER.trace(() -> String.format("Request: %s", request));
final String response;
try {
this.socket.getOutputStream().write(request.getBytes());
response = scanner.next();
} catch (IOException | NoSuchElementException e) {
return null;
}
LOGGER.trace(() -> String.format("Response: %s", response));
if (response.isEmpty())
return null;
JSONObject responseJson = (JSONObject) JSONValue.parse(response);
if (responseJson == null)
return null;
return responseJson.get("result");
}
}

View File

@ -0,0 +1,42 @@
package org.qortal.crypto;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public abstract class TrustlessSSLSocketFactory {
// Create a trust manager that does not validate certificate chains
private static final TrustManager[] TRUSTLESS_MANAGER = new TrustManager[] {
new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
}
};
// Install the all-trusting trust manager
private static final SSLContext sc;
static {
try {
sc = SSLContext.getInstance("SSL");
sc.init(null, TRUSTLESS_MANAGER, new java.security.SecureRandom());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static SSLSocketFactory getSocketFactory() {
return sc.getSocketFactory();
}
}

View File

@ -26,4 +26,9 @@ public class BitTwiddling {
return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) };
}
/** Convert little-endian bytes to int */
public static int fromLEBytes(byte[] bytes, int offset) {
return (bytes[offset] & 0xff) | (bytes[offset + 1] & 0xff) << 8 | (bytes[offset + 2] & 0xff) << 16 | (bytes[offset + 3] & 0xff) << 24;
}
}

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);