forked from Qortal/qortal
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:
parent
59de22883b
commit
3d4fc38fcb
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()) {
|
||||
|
387
src/main/java/org/qortal/crosschain/ElectrumX.java
Normal file
387
src/main/java/org/qortal/crosschain/ElectrumX.java
Normal 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");
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user