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.NetworkParameters;
|
||||||
import org.bitcoinj.core.TransactionOutput;
|
import org.bitcoinj.core.TransactionOutput;
|
||||||
import org.bitcoinj.script.Script.ScriptType;
|
import org.bitcoinj.script.Script.ScriptType;
|
||||||
import org.bitcoinj.wallet.WalletTransaction;
|
|
||||||
import org.qortal.account.PublicKeyAccount;
|
import org.qortal.account.PublicKeyAccount;
|
||||||
import org.qortal.api.ApiError;
|
import org.qortal.api.ApiError;
|
||||||
import org.qortal.api.ApiErrors;
|
import org.qortal.api.ApiErrors;
|
||||||
@ -452,7 +451,7 @@ public class CrossChainResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), crossChainTradeData.lockTime, redeemBitcoinAddress.getHash(), crossChainTradeData.secretHash);
|
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);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
return p2shAddress.toString();
|
return p2shAddress.toString();
|
||||||
@ -522,22 +521,19 @@ public class CrossChainResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), crossChainTradeData.lockTime, redeemBitcoinAddress.getHash(), crossChainTradeData.secretHash);
|
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);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
|
|
||||||
Long medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
Integer medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||||
if (medianBlockTime == null)
|
if (medianBlockTime == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||||
|
|
||||||
long now = NTP.getTime();
|
long now = NTP.getTime();
|
||||||
|
|
||||||
// Check P2SH is funded
|
// 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)
|
if (p2shBalance == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||||
|
|
||||||
@ -545,6 +541,8 @@ public class CrossChainResource {
|
|||||||
p2shStatus.bitcoinP2shAddress = p2shAddress.toString();
|
p2shStatus.bitcoinP2shAddress = p2shAddress.toString();
|
||||||
p2shStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance.value, 8);
|
p2shStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance.value, 8);
|
||||||
|
|
||||||
|
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||||
|
|
||||||
if (p2shBalance.value >= crossChainTradeData.expectedBitcoin && fundingOutputs.size() == 1) {
|
if (p2shBalance.value >= crossChainTradeData.expectedBitcoin && fundingOutputs.size() == 1) {
|
||||||
p2shStatus.canRedeem = now >= medianBlockTime * 1000L;
|
p2shStatus.canRedeem = now >= medianBlockTime * 1000L;
|
||||||
p2shStatus.canRefund = now >= crossChainTradeData.lockTime * 1000L;
|
p2shStatus.canRefund = now >= crossChainTradeData.lockTime * 1000L;
|
||||||
@ -552,7 +550,8 @@ public class CrossChainResource {
|
|||||||
|
|
||||||
if (now >= medianBlockTime * 1000L) {
|
if (now >= medianBlockTime * 1000L) {
|
||||||
// See if we can extract secret
|
// 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;
|
return p2shStatus;
|
||||||
@ -630,20 +629,19 @@ public class CrossChainResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), crossChainTradeData.lockTime, redeemBitcoinAddress.getHash(), crossChainTradeData.secretHash);
|
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);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
|
|
||||||
long now = NTP.getTime();
|
long now = NTP.getTime();
|
||||||
|
|
||||||
// Check P2SH is funded
|
// 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)
|
if (p2shBalance == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||||
|
|
||||||
|
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||||
if (fundingOutputs.size() != 1)
|
if (fundingOutputs.size() != 1)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||||
|
|
||||||
@ -741,24 +739,22 @@ public class CrossChainResource {
|
|||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), crossChainTradeData.lockTime, redeemAddress.getHash(), crossChainTradeData.secretHash);
|
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);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
|
|
||||||
Long medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
Integer medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||||
if (medianBlockTime == null)
|
if (medianBlockTime == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE);
|
||||||
|
|
||||||
long now = NTP.getTime();
|
long now = NTP.getTime();
|
||||||
|
|
||||||
// Check P2SH is funded
|
// Check P2SH is funded
|
||||||
final int startTime = (int) (crossChainTradeData.tradeModeTimestamp / 1000L);
|
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||||
List<TransactionOutput> fundingOutputs = new ArrayList<>();
|
|
||||||
|
|
||||||
Coin p2shBalance = BTC.getInstance().getBalanceAndOtherInfo(p2shAddress.toString(), startTime, fundingOutputs, null);
|
|
||||||
if (p2shBalance == null)
|
if (p2shBalance == null)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
|
||||||
|
|
||||||
|
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
|
||||||
if (fundingOutputs.size() != 1)
|
if (fundingOutputs.size() != 1)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
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.BlockMinter;
|
||||||
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
||||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||||
import org.qortal.crosschain.BTC;
|
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.account.MintingAccountData;
|
import org.qortal.data.account.MintingAccountData;
|
||||||
import org.qortal.data.account.RewardShareData;
|
import org.qortal.data.account.RewardShareData;
|
||||||
@ -676,9 +675,6 @@ public class Controller extends Thread {
|
|||||||
if (!isStopping) {
|
if (!isStopping) {
|
||||||
isStopping = true;
|
isStopping = true;
|
||||||
|
|
||||||
LOGGER.info("Shutting down Bitcoin support");
|
|
||||||
BTC.shutdown();
|
|
||||||
|
|
||||||
LOGGER.info("Shutting down API");
|
LOGGER.info("Shutting down API");
|
||||||
ApiService.getInstance().stop();
|
ApiService.getInstance().stop();
|
||||||
|
|
||||||
|
@ -1,68 +1,24 @@
|
|||||||
package org.qortal.crosschain;
|
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.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.TreeMap;
|
import java.util.stream.Collectors;
|
||||||
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 org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.bitcoinj.core.Address;
|
import org.bitcoinj.core.Address;
|
||||||
import org.bitcoinj.core.BlockChain;
|
|
||||||
import org.bitcoinj.core.CheckpointManager;
|
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bitcoinj.core.ECKey;
|
|
||||||
import org.bitcoinj.core.NetworkParameters;
|
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.Transaction;
|
||||||
import org.bitcoinj.core.TransactionBroadcast;
|
|
||||||
import org.bitcoinj.core.TransactionOutput;
|
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.MainNetParams;
|
||||||
import org.bitcoinj.params.RegTestParams;
|
import org.bitcoinj.params.RegTestParams;
|
||||||
import org.bitcoinj.params.TestNet3Params;
|
import org.bitcoinj.params.TestNet3Params;
|
||||||
import org.bitcoinj.script.Script.ScriptType;
|
import org.bitcoinj.script.ScriptBuilder;
|
||||||
import org.bitcoinj.store.BlockStore;
|
|
||||||
import org.bitcoinj.store.BlockStoreException;
|
|
||||||
import org.bitcoinj.store.MemoryBlockStore;
|
|
||||||
import org.bitcoinj.utils.MonetaryFormat;
|
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.settings.Settings;
|
||||||
|
import org.qortal.utils.BitTwiddling;
|
||||||
|
import org.qortal.utils.Pair;
|
||||||
|
|
||||||
public class BTC {
|
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 long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
|
||||||
public static final int HASH160_LENGTH = 20;
|
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);
|
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
|
||||||
|
|
||||||
|
private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
|
||||||
|
|
||||||
public enum BitcoinNet {
|
public enum BitcoinNet {
|
||||||
MAIN {
|
MAIN {
|
||||||
@Override
|
@Override
|
||||||
@ -107,156 +54,9 @@ public class BTC {
|
|||||||
public abstract NetworkParameters getParams();
|
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 static BTC instance;
|
||||||
private enum RunningState { RUNNING, STOPPED };
|
|
||||||
FutureTask<RunningState> startupFuture;
|
|
||||||
|
|
||||||
private final NetworkParameters params;
|
private final NetworkParameters params;
|
||||||
private final String checkpointsFileName;
|
private final ElectrumX electrumX;
|
||||||
private final File directory;
|
|
||||||
|
|
||||||
private PeerGroup peerGroup;
|
|
||||||
private BlockStore blockStore;
|
|
||||||
private ResettableBlockChain chain;
|
|
||||||
|
|
||||||
private UpdateableCheckpointManager manager;
|
|
||||||
|
|
||||||
// Constructors and instance
|
// Constructors and instance
|
||||||
|
|
||||||
@ -264,399 +64,96 @@ public class BTC {
|
|||||||
BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet();
|
BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet();
|
||||||
this.params = bitcoinNet.getParams();
|
this.params = bitcoinNet.getParams();
|
||||||
|
|
||||||
switch (bitcoinNet) {
|
LOGGER.info(() -> String.format("Starting Bitcoin support using %s", bitcoinNet.name()));
|
||||||
case MAIN:
|
|
||||||
this.checkpointsFileName = "checkpoints.txt";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case TEST3:
|
this.electrumX = ElectrumX.getInstance(bitcoinNet.name());
|
||||||
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");
|
public static synchronized BTC getInstance() {
|
||||||
|
if (instance == null)
|
||||||
startupFuture = new FutureTask<>(BTC::startUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static BTC getInstance() {
|
|
||||||
synchronized (instanceLock) {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = new BTC();
|
instance = new BTC();
|
||||||
Executors.newSingleThreadExecutor().execute(instance.startupFuture);
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Getters & setters
|
// Getters & setters
|
||||||
|
|
||||||
/* package */ File getDirectory() {
|
|
||||||
return this.directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* package */ String getCheckpointsFileName() {
|
|
||||||
return this.checkpointsFileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public NetworkParameters getNetworkParameters() {
|
public NetworkParameters getNetworkParameters() {
|
||||||
return this.params;
|
return this.params;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static utility methods
|
public static synchronized void resetForTesting() {
|
||||||
|
|
||||||
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;
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actual useful methods for use by other classes
|
// Actual useful methods for use by other classes
|
||||||
|
|
||||||
/** Returns median timestamp from latest 11 blocks, in seconds. */
|
/** Returns median timestamp from latest 11 blocks, in seconds. */
|
||||||
public Long getMedianBlockTime() {
|
public Integer getMedianBlockTime() {
|
||||||
if (!this.isRunning())
|
Integer height = this.electrumX.getCurrentHeight();
|
||||||
// Failed to start up, or we're shutting down
|
if (height == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// 11 blocks, at roughly 10 minutes per block, means we should go back at least 110 minutes
|
// Grab latest 11 blocks
|
||||||
// but some blocks have been way longer than 10 minutes, so be massively pessimistic
|
List<byte[]> blockHeaders = this.electrumX.getBlockHeaders(height, 11);
|
||||||
int startTime = (int) (System.currentTimeMillis() / 1000L) - 11 * 60 * 60; // 11 hours before now, in seconds
|
if (blockHeaders == null || blockHeaders.size() < 11)
|
||||||
|
|
||||||
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;
|
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...
|
// Descending, but order shouldn't matter as we're picking median...
|
||||||
latestBlocks.sort((a, b) -> Long.compare(b.getHeader().getTimeSeconds(), a.getHeader().getTimeSeconds()));
|
blockTimestamps.sort((a, b) -> Integer.compare(b, a));
|
||||||
|
|
||||||
return latestBlocks.get(5).getHeader().getTimeSeconds();
|
return blockTimestamps.get(5);
|
||||||
} catch (BlockStoreException e) {
|
|
||||||
LOGGER.error(String.format("Can't get Bitcoin median block time due to blockstore issue: %s", e.getMessage()));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Coin getBalance(String base58Address, int startTime) {
|
public Coin getBalance(String base58Address) {
|
||||||
if (!this.isRunning())
|
Long balance = this.electrumX.getBalance(addressToScript(base58Address));
|
||||||
// Failed to start up, or we're shutting down
|
if (balance == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Create new wallet containing only the address we're interested in, ignoring anything prior to startTime
|
return Coin.valueOf(balance);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<TransactionOutput> getOutputs(String base58Address, int startTime) {
|
public List<TransactionOutput> getUnspentOutputs(String base58Address) {
|
||||||
if (!this.isRunning())
|
List<Pair<byte[], Integer>> unspentOutputs = this.electrumX.getUnspentOutputs(addressToScript(base58Address));
|
||||||
// Failed to start up, or we're shutting down
|
if (unspentOutputs == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
Wallet wallet = createEmptyWallet();
|
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
|
||||||
Address address = Address.fromString(this.params, base58Address);
|
for (Pair<byte[], Integer> unspentOutput : unspentOutputs) {
|
||||||
wallet.addWatchedAddress(address, startTime);
|
List<TransactionOutput> transactionOutputs = getOutputs(unspentOutput.getA());
|
||||||
|
if (transactionOutputs == 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// Create new wallet containing only the address we're interested in, ignoring anything prior to startTime
|
unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.getB()));
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<TransactionOutput> getOutputs(byte[] txId, int startTime) {
|
return unspentTransactionOutputs;
|
||||||
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;
|
return null;
|
||||||
|
|
||||||
Wallet wallet = createEmptyWallet();
|
Transaction transaction = new Transaction(this.params, rawTransactionBytes);
|
||||||
|
return transaction.getOutputs();
|
||||||
// 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));
|
public List<byte[]> getAddressTransactions(String base58Address) {
|
||||||
|
return this.electrumX.getAddressTransactions(addressToScript(base58Address));
|
||||||
// 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 boolean broadcastTransaction(Transaction transaction) {
|
public boolean broadcastTransaction(Transaction transaction) {
|
||||||
if (!this.isRunning())
|
return this.electrumX.broadcastTransaction(transaction.bitcoinSerialize());
|
||||||
// Failed to start up, or we're shutting down
|
|
||||||
return false;
|
|
||||||
|
|
||||||
TransactionBroadcast transactionBroadcast = this.peerGroup.broadcastTransaction(transaction);
|
|
||||||
|
|
||||||
try {
|
|
||||||
transactionBroadcast.future().get();
|
|
||||||
return true;
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Utility methods for us
|
||||||
|
|
||||||
|
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.ScriptChunk;
|
||||||
import org.bitcoinj.script.ScriptOpCodes;
|
import org.bitcoinj.script.ScriptOpCodes;
|
||||||
import org.bitcoinj.script.Script.ScriptType;
|
import org.bitcoinj.script.Script.ScriptType;
|
||||||
import org.bitcoinj.wallet.WalletTransaction;
|
|
||||||
import org.ciyam.at.API;
|
import org.ciyam.at.API;
|
||||||
import org.ciyam.at.CompilationException;
|
import org.ciyam.at.CompilationException;
|
||||||
import org.ciyam.at.FunctionCode;
|
import org.ciyam.at.FunctionCode;
|
||||||
@ -619,11 +618,11 @@ public class BTCACCT {
|
|||||||
return tradeData;
|
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();
|
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
||||||
|
|
||||||
for (WalletTransaction walletTransaction : walletTransactions) {
|
for (byte[] rawTransaction : rawTransactions) {
|
||||||
Transaction transaction = walletTransaction.getTransaction();
|
Transaction transaction = new Transaction(params, rawTransaction);
|
||||||
|
|
||||||
// Cycle through inputs, looking for one that spends our P2SH
|
// Cycle through inputs, looking for one that spends our P2SH
|
||||||
for (TransactionInput input : transaction.getInputs()) {
|
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) };
|
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 static org.junit.Assert.*;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
@ -11,7 +10,6 @@ import java.util.concurrent.Executors;
|
|||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
import org.bitcoinj.store.BlockStoreException;
|
import org.bitcoinj.store.BlockStoreException;
|
||||||
import org.bitcoinj.wallet.WalletTransaction;
|
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@ -37,12 +35,10 @@ public class BtcTests extends Common {
|
|||||||
BTC btc = BTC.getInstance();
|
BTC btc = BTC.getInstance();
|
||||||
|
|
||||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
Future<Long> future = executor.submit(() -> btc.getMedianBlockTime());
|
Future<Integer> future = executor.submit(() -> btc.getMedianBlockTime());
|
||||||
|
|
||||||
BTC.shutdown();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Long medianBlockTime = future.get();
|
Integer medianBlockTime = future.get();
|
||||||
assertNull("Shutdown should occur before we get a result", medianBlockTime);
|
assertNull("Shutdown should occur before we get a result", medianBlockTime);
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
}
|
}
|
||||||
@ -55,12 +51,10 @@ public class BtcTests extends Common {
|
|||||||
BTC btc = BTC.getInstance();
|
BTC btc = BTC.getInstance();
|
||||||
|
|
||||||
ExecutorService executor = Executors.newSingleThreadExecutor();
|
ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
Future<Long> future = executor.submit(() -> btc.getMedianBlockTime());
|
Future<Integer> future = executor.submit(() -> btc.getMedianBlockTime());
|
||||||
|
|
||||||
BTC.shutdown();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Long medianBlockTime = future.get();
|
Integer medianBlockTime = future.get();
|
||||||
assertNull("Shutdown should occur before we get a result", medianBlockTime);
|
assertNull("Shutdown should occur before we get a result", medianBlockTime);
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
}
|
}
|
||||||
@ -92,14 +86,11 @@ public class BtcTests extends Common {
|
|||||||
public void testFindP2shSecret() {
|
public void testFindP2shSecret() {
|
||||||
// This actually exists on TEST3 but can take a while to fetch
|
// This actually exists on TEST3 but can take a while to fetch
|
||||||
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
|
||||||
int startTime = 1587510000; // Tue 21 Apr 2020 23:00:00 UTC
|
|
||||||
|
|
||||||
List<WalletTransaction> walletTransactions = new ArrayList<>();
|
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
|
||||||
|
|
||||||
BTC.getInstance().getBalanceAndOtherInfo(p2shAddress, startTime, null, walletTransactions);
|
|
||||||
|
|
||||||
byte[] expectedSecret = AtTests.secret;
|
byte[] expectedSecret = AtTests.secret;
|
||||||
byte[] secret = BTCACCT.findP2shSecret(p2shAddress, walletTransactions);
|
byte[] secret = BTCACCT.findP2shSecret(p2shAddress, rawTransactions);
|
||||||
|
|
||||||
assertNotNull(secret);
|
assertNotNull(secret);
|
||||||
assertTrue("secret incorrect", Arrays.equals(expectedSecret, 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.controller.Controller;
|
||||||
import org.qortal.crosschain.BTC;
|
import org.qortal.crosschain.BTC;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCACCT;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryFactory;
|
import org.qortal.repository.RepositoryFactory;
|
||||||
@ -105,7 +106,7 @@ public class BuildP2SH {
|
|||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
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);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
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.controller.Controller;
|
||||||
import org.qortal.crosschain.BTC;
|
import org.qortal.crosschain.BTC;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCACCT;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryFactory;
|
import org.qortal.repository.RepositoryFactory;
|
||||||
@ -115,7 +116,7 @@ public class CheckP2SH {
|
|||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
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);
|
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
|
|
||||||
if (!derivedP2shAddress.equals(p2shAddress)) {
|
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)));
|
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
|
// Check P2SH is funded
|
||||||
final int startTime = lockTime - 86400;
|
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||||
|
|
||||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
|
|
||||||
if (p2shBalance == null) {
|
if (p2shBalance == null) {
|
||||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||||
System.exit(2);
|
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)));
|
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)
|
// 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) {
|
if (fundingOutputs == null) {
|
||||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||||
System.exit(2);
|
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)
|
if (error != null)
|
||||||
System.err.println(error);
|
System.err.println(error);
|
||||||
|
|
||||||
System.err.println(String.format("usage: GetTransaction <bitcoin-tx> <start-time>"));
|
System.err.println(String.format("usage: GetTransaction <bitcoin-tx>"));
|
||||||
System.err.println(String.format("example (mainnet): GetTransaction 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660 1585317000"));
|
System.err.println(String.format("example (mainnet): GetTransaction 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660"));
|
||||||
System.err.println(String.format("example (testnet): GetTransaction 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e 1584376000"));
|
System.err.println(String.format("example (testnet): GetTransaction 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e"));
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
if (args.length < 2 || args.length > 2)
|
if (args.length != 1)
|
||||||
usage(null);
|
usage(null);
|
||||||
|
|
||||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||||
Settings.fileInstance("settings-test.json");
|
Settings.fileInstance("settings-test.json");
|
||||||
|
|
||||||
byte[] transactionId = null;
|
byte[] transactionId = null;
|
||||||
int startTime = 0;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
int argIndex = 0;
|
int argIndex = 0;
|
||||||
|
|
||||||
transactionId = HashCode.fromString(args[argIndex++]).asBytes();
|
transactionId = HashCode.fromString(args[argIndex++]).asBytes();
|
||||||
|
|
||||||
startTime = Integer.parseInt(args[argIndex++]);
|
|
||||||
} catch (NumberFormatException | AddressFormatException e) {
|
} catch (NumberFormatException | AddressFormatException e) {
|
||||||
usage(String.format("Argument format exception: %s", e.getMessage()));
|
usage(String.format("Argument format exception: %s", e.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab all outputs from transaction
|
// Grab all outputs from transaction
|
||||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(transactionId, startTime);
|
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(transactionId);
|
||||||
if (fundingOutputs == null) {
|
if (fundingOutputs == null) {
|
||||||
System.out.println(String.format("Transaction not found"));
|
System.out.println(String.format("Transaction not found"));
|
||||||
return;
|
return;
|
||||||
|
@ -19,6 +19,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.crosschain.BTC;
|
import org.qortal.crosschain.BTC;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCACCT;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryFactory;
|
import org.qortal.repository.RepositoryFactory;
|
||||||
@ -111,7 +112,7 @@ public class Redeem {
|
|||||||
|
|
||||||
// New/derived info
|
// 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)));
|
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
|
||||||
|
|
||||||
ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey);
|
ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey);
|
||||||
@ -123,7 +124,7 @@ public class Redeem {
|
|||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
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);
|
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
|
|
||||||
if (!derivedP2shAddress.equals(p2shAddress)) {
|
if (!derivedP2shAddress.equals(p2shAddress)) {
|
||||||
@ -146,9 +147,7 @@ public class Redeem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check P2SH is funded
|
// Check P2SH is funded
|
||||||
final int startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400;
|
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||||
|
|
||||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
|
|
||||||
if (p2shBalance == null) {
|
if (p2shBalance == null) {
|
||||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||||
System.exit(2);
|
System.exit(2);
|
||||||
@ -156,7 +155,7 @@ public class Redeem {
|
|||||||
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
|
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)
|
// 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) {
|
if (fundingOutputs == null) {
|
||||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||||
System.exit(2);
|
System.exit(2);
|
||||||
|
@ -19,6 +19,7 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.crosschain.BTC;
|
import org.qortal.crosschain.BTC;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCACCT;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryFactory;
|
import org.qortal.repository.RepositoryFactory;
|
||||||
@ -122,7 +123,7 @@ public class Refund {
|
|||||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
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);
|
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
|
|
||||||
if (!derivedP2shAddress.equals(p2shAddress)) {
|
if (!derivedP2shAddress.equals(p2shAddress)) {
|
||||||
@ -150,9 +151,7 @@ public class Refund {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check P2SH is funded
|
// Check P2SH is funded
|
||||||
final int startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400;
|
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString());
|
||||||
|
|
||||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
|
|
||||||
if (p2shBalance == null) {
|
if (p2shBalance == null) {
|
||||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||||
System.exit(2);
|
System.exit(2);
|
||||||
@ -160,7 +159,7 @@ public class Refund {
|
|||||||
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
|
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)
|
// 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) {
|
if (fundingOutputs == null) {
|
||||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||||
System.exit(2);
|
System.exit(2);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user