mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-22 20:26:50 +00:00
work in progress: btc-qort cross-chain trades
Streamlined BTC class and switched to memory block store. Split BTCACCTTests into BTCACCT utility class and (so far) three stand-alone apps: Initiate1, Refund2 and Respond2 Moved some Qortal-specific CIYAM AT constants into blockchain config. Removed redundant BTCTests
This commit is contained in:
@@ -6,14 +6,15 @@ import java.nio.ByteOrder;
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.InsufficientMoneyException;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Sha256Hash;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionOutPoint;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.core.Transaction.SigHash;
|
||||
import org.bitcoinj.core.TransactionInput;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.crypto.TransactionSignature;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.wallet.Wallet;
|
||||
import org.bitcoinj.script.ScriptChunk;
|
||||
import org.bitcoinj.script.ScriptOpCodes;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.ciyam.at.FunctionCode;
|
||||
import org.ciyam.at.MachineState;
|
||||
@@ -25,6 +26,8 @@ import com.google.common.primitives.Bytes;
|
||||
|
||||
public class BTCACCT {
|
||||
|
||||
public static final Coin DEFAULT_BTC_FEE = Coin.valueOf(1000L); // 0.00001000 BTC
|
||||
|
||||
private static final byte[] redeemScript1 = HashCode.fromString("76a820").asBytes(); // OP_DUP OP_SHA256 push(0x20 bytes)
|
||||
private static final byte[] redeemScript2 = HashCode.fromString("87637576a914").asBytes(); // OP_EQUAL OP_IF OP_DROP OP_DUP OP_HASH160 push(0x14 bytes)
|
||||
private static final byte[] redeemScript3 = HashCode.fromString("88ac6704").asBytes(); // OP_EQUALVERIFY OP_CHECKSIG OP_ELSE push(0x4 bytes)
|
||||
@@ -60,6 +63,48 @@ public class BTCACCT {
|
||||
redeemScript4, senderPubKeyHash160, redeemScript5);
|
||||
}
|
||||
|
||||
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey senderKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) {
|
||||
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
||||
|
||||
Transaction refundTransaction = new Transaction(params);
|
||||
refundTransaction.setVersion(2);
|
||||
|
||||
refundAmount = refundAmount.subtract(DEFAULT_BTC_FEE);
|
||||
|
||||
// Output is back to P2SH funder
|
||||
refundTransaction.addOutput(refundAmount, ScriptBuilder.createOutputScript(Address.fromKey(params, senderKey, ScriptType.P2PKH)));
|
||||
|
||||
// Input (without scriptSig prior to signing)
|
||||
TransactionInput input = new TransactionInput(params, null, new byte[0], fundingOutput.getOutPointFor());
|
||||
input.setSequenceNumber(0); // Use 0, not max-value, so lockTime can be used
|
||||
refundTransaction.addInput(input);
|
||||
|
||||
// Set locktime after inputs added but before input signatures are generated
|
||||
refundTransaction.setLockTime(lockTime);
|
||||
|
||||
// Generate transaction signature for input
|
||||
final boolean anyoneCanPay = false;
|
||||
TransactionSignature txSig = refundTransaction.calculateSignature(0, senderKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
|
||||
|
||||
// Build scriptSig with...
|
||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||
|
||||
// transaction signature
|
||||
byte[] txSigBytes = txSig.encodeToBitcoin();
|
||||
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
|
||||
|
||||
// sender's public key
|
||||
byte[] senderPubKey = senderKey.getPubKey();
|
||||
scriptBuilder.addChunk(new ScriptChunk(senderPubKey.length, senderPubKey));
|
||||
|
||||
/// redeem script
|
||||
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
||||
|
||||
refundTransaction.getInput(0).setScriptSig(scriptBuilder.build());
|
||||
|
||||
return refundTransaction;
|
||||
}
|
||||
|
||||
public static byte[] buildCiyamAT(byte[] secretHash, byte[] destinationQortalPubKey, long refundMinutes) {
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0;
|
||||
|
@@ -1,134 +0,0 @@
|
||||
package org.qortal.at;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.Timestamp;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.ATTransactionData;
|
||||
import org.qortal.data.transaction.PaymentTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.BlockRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.transaction.Transaction;
|
||||
|
||||
public enum BlockchainAPI {
|
||||
|
||||
QORTAL(0) {
|
||||
@Override
|
||||
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
|
||||
int height = timestamp.blockHeight;
|
||||
int sequence = timestamp.transactionSequence + 1;
|
||||
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
BlockRepository blockRepository = api.repository.getBlockRepository();
|
||||
|
||||
try {
|
||||
Account recipientAccount = new Account(api.repository, recipient);
|
||||
|
||||
while (height <= blockRepository.getBlockchainHeight()) {
|
||||
BlockData blockData = blockRepository.fromHeight(height);
|
||||
|
||||
if (blockData == null)
|
||||
throw new DataException("Unable to fetch block " + height + " from repository?");
|
||||
|
||||
Block block = new Block(api.repository, blockData);
|
||||
|
||||
List<Transaction> transactions = block.getTransactions();
|
||||
|
||||
// No more transactions in this block? Try next block
|
||||
if (sequence >= transactions.size()) {
|
||||
++height;
|
||||
sequence = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
Transaction transaction = transactions.get(sequence);
|
||||
|
||||
// Transaction needs to be sent to specified recipient
|
||||
if (transaction.getRecipientAccounts().contains(recipientAccount)) {
|
||||
// Found a transaction
|
||||
|
||||
api.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue());
|
||||
|
||||
// Hash transaction's signature into other three A fields for future verification that it's the same transaction
|
||||
byte[] hash = QortalATAPI.sha192(transaction.getTransactionData().getSignature());
|
||||
|
||||
api.setA2(state, QortalATAPI.fromBytes(hash, 0));
|
||||
api.setA3(state, QortalATAPI.fromBytes(hash, 8));
|
||||
api.setA4(state, QortalATAPI.fromBytes(hash, 16));
|
||||
return;
|
||||
}
|
||||
|
||||
// Transaction wasn't for us - keep going
|
||||
++sequence;
|
||||
}
|
||||
|
||||
// No more transactions - zero A and exit
|
||||
api.zeroA(state);
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch next transaction?", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) {
|
||||
QortalATAPI api = (QortalATAPI) state.getAPI();
|
||||
TransactionData transactionData = api.fetchTransaction(state);
|
||||
|
||||
switch (transactionData.getType()) {
|
||||
case PAYMENT:
|
||||
return ((PaymentTransactionData) transactionData).getAmount().unscaledValue().longValue();
|
||||
|
||||
case AT:
|
||||
BigDecimal amount = ((ATTransactionData) transactionData).getAmount();
|
||||
|
||||
if (amount != null)
|
||||
return amount.unscaledValue().longValue();
|
||||
else
|
||||
return 0xffffffffffffffffL;
|
||||
|
||||
default:
|
||||
return 0xffffffffffffffffL;
|
||||
}
|
||||
}
|
||||
},
|
||||
BTC(1) {
|
||||
@Override
|
||||
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
|
||||
// TODO BTC transaction support for ATv2
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) {
|
||||
// TODO BTC transaction support for ATv2
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
public final int value;
|
||||
|
||||
private static final Map<Integer, BlockchainAPI> map = stream(BlockchainAPI.values()).collect(toMap(type -> type.value, type -> type));
|
||||
|
||||
BlockchainAPI(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static BlockchainAPI valueOf(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
// Blockchain-specific API methods
|
||||
|
||||
public abstract void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state);
|
||||
|
||||
public abstract long getAmountFromTransactionInA(Timestamp timestamp, MachineState state);
|
||||
|
||||
}
|
@@ -17,6 +17,8 @@ import org.qortal.account.Account;
|
||||
import org.qortal.account.GenesisAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.block.BlockChain.CiyamAtSettings;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
@@ -33,16 +35,11 @@ import com.google.common.primitives.Bytes;
|
||||
|
||||
public class QortalATAPI extends API {
|
||||
|
||||
// Useful constants
|
||||
private static final BigDecimal FEE_PER_STEP = BigDecimal.valueOf(1.0).setScale(8); // 1 QORT per "step"
|
||||
private static final int MAX_STEPS_PER_ROUND = 500;
|
||||
private static final int STEPS_PER_FUNCTION_CALL = 10;
|
||||
private static final int MINUTES_PER_BLOCK = 10;
|
||||
|
||||
// Properties
|
||||
Repository repository;
|
||||
ATData atData;
|
||||
long blockTimestamp;
|
||||
private Repository repository;
|
||||
private ATData atData;
|
||||
private long blockTimestamp;
|
||||
private final CiyamAtSettings ciyamAtSettings;
|
||||
|
||||
/** List of generated AT transactions */
|
||||
List<AtTransaction> transactions;
|
||||
@@ -54,36 +51,42 @@ public class QortalATAPI extends API {
|
||||
this.atData = atData;
|
||||
this.transactions = new ArrayList<>();
|
||||
this.blockTimestamp = blockTimestamp;
|
||||
|
||||
this.ciyamAtSettings = BlockChain.getInstance().getCiyamAtSettings();
|
||||
}
|
||||
|
||||
// Methods specific to Qortal AT processing, not inherited
|
||||
|
||||
public Repository getRepository() {
|
||||
return this.repository;
|
||||
}
|
||||
|
||||
public List<AtTransaction> getTransactions() {
|
||||
return this.transactions;
|
||||
}
|
||||
|
||||
public BigDecimal calcFinalFees(MachineState state) {
|
||||
return FEE_PER_STEP.multiply(BigDecimal.valueOf(state.getSteps()));
|
||||
return this.ciyamAtSettings.feePerStep.multiply(BigDecimal.valueOf(state.getSteps()));
|
||||
}
|
||||
|
||||
// Inherited methods from CIYAM AT API
|
||||
|
||||
@Override
|
||||
public int getMaxStepsPerRound() {
|
||||
return MAX_STEPS_PER_ROUND;
|
||||
return this.ciyamAtSettings.maxStepsPerRound;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpCodeSteps(OpCode opcode) {
|
||||
if (opcode.value >= OpCode.EXT_FUN.value && opcode.value <= OpCode.EXT_FUN_RET_DAT_2.value)
|
||||
return STEPS_PER_FUNCTION_CALL;
|
||||
return this.ciyamAtSettings.stepsPerFunctionCall;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFeePerStep() {
|
||||
return FEE_PER_STEP.unscaledValue().longValue();
|
||||
return this.ciyamAtSettings.feePerStep.unscaledValue().longValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -303,7 +306,7 @@ public class QortalATAPI extends API {
|
||||
int blockHeight = timestamp.blockHeight;
|
||||
|
||||
// At least one block in the future
|
||||
blockHeight += (minutes / MINUTES_PER_BLOCK) + 1;
|
||||
blockHeight += (minutes / this.ciyamAtSettings.minutesPerBlock) + 1;
|
||||
|
||||
return new Timestamp(blockHeight, 0).longValue();
|
||||
}
|
||||
|
@@ -155,6 +155,18 @@ public class BlockChain {
|
||||
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
|
||||
private long onlineAccountSignaturesMaxLifetime;
|
||||
|
||||
/** Settings relating to CIYAM AT feature. */
|
||||
public static class CiyamAtSettings {
|
||||
/** Fee per step/op-code executed. */
|
||||
public BigDecimal feePerStep;
|
||||
/** Maximum number of steps per execution round, before AT is forced to sleep until next block. */
|
||||
public int maxStepsPerRound;
|
||||
/** How many steps for calling a function. */
|
||||
public int stepsPerFunctionCall;
|
||||
/** Roughly how many minutes per block. */
|
||||
public int minutesPerBlock;
|
||||
}
|
||||
private CiyamAtSettings ciyamAtSettings;
|
||||
|
||||
// Constructors, etc.
|
||||
|
||||
@@ -342,6 +354,10 @@ public class BlockChain {
|
||||
return this.onlineAccountSignaturesMaxLifetime;
|
||||
}
|
||||
|
||||
public CiyamAtSettings getCiyamAtSettings() {
|
||||
return this.ciyamAtSettings;
|
||||
}
|
||||
|
||||
// Convenience methods for specific blockchain feature triggers
|
||||
|
||||
public long getMessageReleaseHeight() {
|
||||
@@ -437,6 +453,9 @@ public class BlockChain {
|
||||
if (this.founderEffectiveMintingLevel <= 0)
|
||||
Settings.throwValidationError("Invalid/missing \"founderEffectiveMintingLevel\" in blockchain config");
|
||||
|
||||
if (this.ciyamAtSettings == null)
|
||||
Settings.throwValidationError("No \"ciyamAtSettings\" entry found in blockchain config");
|
||||
|
||||
if (this.featureTriggers == null)
|
||||
Settings.throwValidationError("No \"featureTriggers\" entry found in blockchain config");
|
||||
|
||||
@@ -452,6 +471,8 @@ public class BlockChain {
|
||||
this.unitFee = this.unitFee.setScale(8);
|
||||
this.minFeePerByte = this.unitFee.divide(this.maxBytesPerUnitFee, MathContext.DECIMAL32);
|
||||
|
||||
this.ciyamAtSettings.feePerStep.setScale(8);
|
||||
|
||||
// Pre-calculate cumulative blocks required for each level
|
||||
int cumulativeBlocks = 0;
|
||||
this.cumulativeBlocksByLevel = new ArrayList<>(this.blocksNeededByLevel.size() + 1);
|
||||
|
@@ -15,35 +15,34 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Date;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
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.PeerGroup;
|
||||
import org.bitcoinj.core.Sha256Hash;
|
||||
import org.bitcoinj.core.StoredBlock;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.core.VerificationException;
|
||||
import org.bitcoinj.core.listeners.NewBestBlockListener;
|
||||
import org.bitcoinj.net.discovery.DnsDiscovery;
|
||||
import org.bitcoinj.params.MainNetParams;
|
||||
import org.bitcoinj.params.TestNet3Params;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.store.BlockStore;
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
import org.bitcoinj.store.SPVBlockStore;
|
||||
import org.bitcoinj.store.MemoryBlockStore;
|
||||
import org.bitcoinj.utils.Threading;
|
||||
import org.bitcoinj.wallet.KeyChainGroup;
|
||||
import org.bitcoinj.wallet.Wallet;
|
||||
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
|
||||
import org.bitcoinj.wallet.listeners.WalletCoinsSentEventListener;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public class BTC {
|
||||
@@ -59,33 +58,23 @@ public class BTC {
|
||||
}
|
||||
}
|
||||
|
||||
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
|
||||
|
||||
private static BTC instance;
|
||||
|
||||
private static File directory;
|
||||
private static String chainFileName;
|
||||
private static String checkpointsFileName;
|
||||
private final NetworkParameters params;
|
||||
private final String checkpointsFileName;
|
||||
private final File directory;
|
||||
|
||||
private static NetworkParameters params;
|
||||
private static PeerGroup peerGroup;
|
||||
private static BlockStore blockStore;
|
||||
|
||||
private static class RollbackBlockChain extends BlockChain {
|
||||
public RollbackBlockChain(NetworkParameters params, BlockStore blockStore) throws BlockStoreException {
|
||||
super(params, blockStore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChainHead(StoredBlock chainHead) throws BlockStoreException {
|
||||
super.setChainHead(chainHead);
|
||||
}
|
||||
}
|
||||
private static RollbackBlockChain chain;
|
||||
private PeerGroup peerGroup;
|
||||
private BlockStore blockStore;
|
||||
private BlockChain chain;
|
||||
|
||||
private static class UpdateableCheckpointManager extends CheckpointManager implements NewBestBlockListener {
|
||||
private static final int checkpointInterval = 500;
|
||||
private static final long CHECKPOINT_THRESHOLD = 7 * 24 * 60 * 60; // seconds
|
||||
|
||||
private static final String minimalTestNet3TextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO\n";
|
||||
private static final String minimalMainNetTextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAABjl7tqvU/FIcDT9gcbVlA4nwtFUbxAtOawZzBpAAAAAKzkcK7NqciBjI/ldojNKncrWleVSgDfBCCn3VRrbSxXaw5/Sf//AB0z8Bkv\n";
|
||||
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) throws IOException {
|
||||
super(params, getMinimalTextFileStream(params));
|
||||
@@ -97,20 +86,35 @@ public class BTC {
|
||||
|
||||
private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params) {
|
||||
if (params == MainNetParams.get())
|
||||
return new ByteArrayInputStream(minimalMainNetTextFile.getBytes());
|
||||
return new ByteArrayInputStream(MINIMAL_MAINNET_TEXTFILE.getBytes());
|
||||
|
||||
if (params == TestNet3Params.get())
|
||||
return new ByteArrayInputStream(minimalTestNet3TextFile.getBytes());
|
||||
return new ByteArrayInputStream(MINIMAL_TESTNET3_TEXTFILE.getBytes());
|
||||
|
||||
throw new RuntimeException("Failed to construct empty UpdateableCheckpointManageer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyNewBestBlock(StoredBlock block) throws VerificationException {
|
||||
int height = block.getHeight();
|
||||
public void notifyNewBestBlock(StoredBlock block) {
|
||||
final int height = block.getHeight();
|
||||
|
||||
if (height % checkpointInterval == 0)
|
||||
checkpoints.put(block.getHeader().getTimeSeconds(), block);
|
||||
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)));
|
||||
checkpoints.put(blockTimestamp, block);
|
||||
|
||||
try {
|
||||
this.saveAsText(new File(BTC.getInstance().getDirectory(), BTC.getInstance().getCheckpointsFileName()));
|
||||
} catch (FileNotFoundException e) {
|
||||
// Save failed - log it but it's not critical
|
||||
LOGGER.warn("Failed to save updated BTC checkpoints: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void saveAsText(File textFile) throws FileNotFoundException {
|
||||
@@ -118,7 +122,9 @@ public class BTC {
|
||||
writer.println("TXT CHECKPOINTS 1");
|
||||
writer.println("0"); // Number of signatures to read. Do this later.
|
||||
writer.println(checkpoints.size());
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
|
||||
|
||||
for (StoredBlock block : checkpoints.values()) {
|
||||
block.serializeCompact(buffer);
|
||||
writer.println(CheckpointManager.BASE64.encode(buffer.array()));
|
||||
@@ -140,7 +146,9 @@ public class BTC {
|
||||
dataOutputStream.writeInt(0); // Number of signatures to read. Do this later.
|
||||
digestOutputStream.on(true);
|
||||
dataOutputStream.writeInt(checkpoints.size());
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
|
||||
|
||||
for (StoredBlock block : checkpoints.values()) {
|
||||
block.serializeCompact(buffer);
|
||||
dataOutputStream.write(buffer.array());
|
||||
@@ -151,9 +159,37 @@ public class BTC {
|
||||
}
|
||||
}
|
||||
}
|
||||
private static UpdateableCheckpointManager manager;
|
||||
private UpdateableCheckpointManager manager;
|
||||
|
||||
// Constructors and instance
|
||||
|
||||
private BTC() {
|
||||
if (Settings.getInstance().useBitcoinTestNet()) {
|
||||
this.params = TestNet3Params.get();
|
||||
this.checkpointsFileName = "checkpoints-testnet.txt";
|
||||
} else {
|
||||
this.params = MainNetParams.get();
|
||||
this.checkpointsFileName = "checkpoints.txt";
|
||||
}
|
||||
|
||||
this.directory = new File("Qortal-BTC");
|
||||
|
||||
if (!this.directory.exists())
|
||||
this.directory.mkdirs();
|
||||
|
||||
File checkpointsFile = new File(this.directory, this.checkpointsFileName);
|
||||
try (InputStream checkpointsStream = new FileInputStream(checkpointsFile)) {
|
||||
this.manager = new UpdateableCheckpointManager(this.params, checkpointsStream);
|
||||
} catch (FileNotFoundException e) {
|
||||
// Construct with no checkpoints then
|
||||
try {
|
||||
this.manager = new UpdateableCheckpointManager(this.params);
|
||||
} catch (IOException e2) {
|
||||
throw new RuntimeException("Failed to create new BTC checkpoints", e2);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load BTC checkpoints", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized BTC getInstance() {
|
||||
@@ -163,160 +199,152 @@ public class BTC {
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Getters & setters
|
||||
|
||||
/* package */ File getDirectory() {
|
||||
return this.directory;
|
||||
}
|
||||
|
||||
/* package */ String getCheckpointsFileName() {
|
||||
return this.checkpointsFileName;
|
||||
}
|
||||
|
||||
/* package */ NetworkParameters getNetworkParameters() {
|
||||
return this.params;
|
||||
}
|
||||
|
||||
// Static utility methods
|
||||
|
||||
public static byte[] hash160(byte[] message) {
|
||||
return RIPE_MD160_DIGESTER.digest(SHA256_DIGESTER.digest(message));
|
||||
}
|
||||
|
||||
public void start() {
|
||||
// Start wallet
|
||||
if (Settings.getInstance().useBitcoinTestNet()) {
|
||||
params = TestNet3Params.get();
|
||||
chainFileName = "bitcoinj-testnet.spvchain";
|
||||
checkpointsFileName = "checkpoints-testnet.txt";
|
||||
} else {
|
||||
params = MainNetParams.get();
|
||||
chainFileName = "bitcoinj.spvchain";
|
||||
checkpointsFileName = "checkpoints.txt";
|
||||
}
|
||||
// Start-up & shutdown
|
||||
private void start(long startTime) throws BlockStoreException {
|
||||
StoredBlock checkpoint = this.manager.getCheckpointBefore(startTime - 1);
|
||||
|
||||
directory = new File("Qortal-BTC");
|
||||
if (!directory.exists())
|
||||
directory.mkdirs();
|
||||
this.blockStore = new MemoryBlockStore(params);
|
||||
this.blockStore.put(checkpoint);
|
||||
this.blockStore.setChainHead(checkpoint);
|
||||
|
||||
File chainFile = new File(directory, chainFileName);
|
||||
this.chain = new BlockChain(this.params, this.blockStore);
|
||||
|
||||
try {
|
||||
blockStore = new SPVBlockStore(params, chainFile);
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException("Failed to open/create BTC SPVBlockStore", e);
|
||||
}
|
||||
|
||||
File checkpointsFile = new File(directory, checkpointsFileName);
|
||||
try (InputStream checkpointsStream = new FileInputStream(checkpointsFile)) {
|
||||
manager = new UpdateableCheckpointManager(params, checkpointsStream);
|
||||
} catch (FileNotFoundException e) {
|
||||
// Construct with no checkpoints then
|
||||
try {
|
||||
manager = new UpdateableCheckpointManager(params);
|
||||
} catch (IOException e2) {
|
||||
throw new RuntimeException("Failed to create new BTC checkpoints", e2);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load BTC checkpoints", e);
|
||||
}
|
||||
|
||||
try {
|
||||
chain = new RollbackBlockChain(params, blockStore);
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException("Failed to construct BTC blockchain", e);
|
||||
}
|
||||
|
||||
peerGroup = new PeerGroup(params, chain);
|
||||
peerGroup.setUserAgent("qortal", "1.0");
|
||||
peerGroup.addPeerDiscovery(new DnsDiscovery(params));
|
||||
peerGroup.start();
|
||||
this.peerGroup = new PeerGroup(this.params, this.chain);
|
||||
this.peerGroup.setUserAgent("qortal", "1.0");
|
||||
this.peerGroup.addPeerDiscovery(new DnsDiscovery(this.params));
|
||||
this.peerGroup.start();
|
||||
}
|
||||
|
||||
public synchronized void shutdown() {
|
||||
if (instance == null)
|
||||
return;
|
||||
|
||||
instance = null;
|
||||
|
||||
peerGroup.stop();
|
||||
|
||||
try {
|
||||
blockStore.close();
|
||||
} catch (BlockStoreException e) {
|
||||
// What can we do?
|
||||
}
|
||||
private void stop() {
|
||||
this.peerGroup.stop();
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
|
||||
protected Wallet createEmptyWallet() {
|
||||
ECKey dummyKey = new ECKey();
|
||||
|
||||
KeyChainGroup keyChainGroup = KeyChainGroup.createBasic(params);
|
||||
keyChainGroup.importKeys(dummyKey);
|
||||
|
||||
Wallet wallet = new Wallet(params, keyChainGroup);
|
||||
|
||||
wallet.removeKey(dummyKey);
|
||||
|
||||
return wallet;
|
||||
return Wallet.createBasic(this.params);
|
||||
}
|
||||
|
||||
public void watch(String base58Address, long startTime) throws InterruptedException, ExecutionException, TimeoutException, BlockStoreException {
|
||||
Wallet wallet = createEmptyWallet();
|
||||
private void replayChain(long startTime, Wallet wallet) throws BlockStoreException {
|
||||
this.start(startTime);
|
||||
|
||||
WalletCoinsReceivedEventListener coinsReceivedListener = new WalletCoinsReceivedEventListener() {
|
||||
@Override
|
||||
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
|
||||
System.out.println("Coins received via transaction " + tx.getTxId().toString());
|
||||
}
|
||||
final WalletCoinsReceivedEventListener coinsReceivedListener = (someWallet, tx, prevBalance, newBalance) -> {
|
||||
LOGGER.debug(String.format("Wallet-related transaction %s", tx.getTxId()));
|
||||
};
|
||||
wallet.addCoinsReceivedEventListener(coinsReceivedListener);
|
||||
|
||||
Address address = Address.fromString(params, base58Address);
|
||||
wallet.addWatchedAddress(address, startTime);
|
||||
final WalletCoinsSentEventListener coinsSentListener = (someWallet, tx, prevBalance, newBalance) -> {
|
||||
LOGGER.debug(String.format("Wallet-related transaction %s", tx.getTxId()));
|
||||
};
|
||||
|
||||
StoredBlock checkpoint = manager.getCheckpointBefore(startTime);
|
||||
blockStore.put(checkpoint);
|
||||
blockStore.setChainHead(checkpoint);
|
||||
chain.setChainHead(checkpoint);
|
||||
if (wallet != null) {
|
||||
wallet.addCoinsReceivedEventListener(coinsReceivedListener);
|
||||
wallet.addCoinsSentEventListener(coinsSentListener);
|
||||
|
||||
chain.addWallet(wallet);
|
||||
peerGroup.addWallet(wallet);
|
||||
peerGroup.setFastCatchupTimeSecs(startTime);
|
||||
|
||||
peerGroup.addBlocksDownloadedEventListener((peer, block, filteredBlock, blocksLeft) -> {
|
||||
if (blocksLeft % 1000 == 0)
|
||||
System.out.println("Blocks left: " + blocksLeft);
|
||||
});
|
||||
|
||||
System.out.println("Starting download...");
|
||||
peerGroup.downloadBlockChain();
|
||||
|
||||
List<TransactionOutput> outputs = wallet.getWatchedOutputs(true);
|
||||
|
||||
peerGroup.removeWallet(wallet);
|
||||
chain.removeWallet(wallet);
|
||||
|
||||
for (TransactionOutput output : outputs)
|
||||
System.out.println(output.toString());
|
||||
}
|
||||
|
||||
public void watch(Script script) {
|
||||
// wallet.addWatchedScripts(scripts);
|
||||
}
|
||||
|
||||
public void updateCheckpoints() {
|
||||
final long now = new Date().getTime() / 1000 - 86400;
|
||||
|
||||
try {
|
||||
StoredBlock checkpoint = manager.getCheckpointBefore(now);
|
||||
blockStore.put(checkpoint);
|
||||
blockStore.setChainHead(checkpoint);
|
||||
chain.setChainHead(checkpoint);
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException("Failed to update BTC checkpoints", e);
|
||||
// Link wallet to chain and peerGroup
|
||||
this.chain.addWallet(wallet);
|
||||
this.peerGroup.addWallet(wallet);
|
||||
}
|
||||
|
||||
peerGroup.setFastCatchupTimeSecs(now);
|
||||
try {
|
||||
// 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 (wallet != null) {
|
||||
wallet.removeCoinsReceivedEventListener(coinsReceivedListener);
|
||||
wallet.removeCoinsSentEventListener(coinsSentListener);
|
||||
|
||||
chain.addNewBestBlockListener(Threading.SAME_THREAD, manager);
|
||||
this.peerGroup.removeWallet(wallet);
|
||||
this.chain.removeWallet(wallet);
|
||||
}
|
||||
|
||||
peerGroup.addBlocksDownloadedEventListener((peer, block, filteredBlock, blocksLeft) -> {
|
||||
if (blocksLeft % 1000 == 0)
|
||||
System.out.println("Blocks left: " + blocksLeft);
|
||||
});
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("Starting download...");
|
||||
peerGroup.downloadBlockChain();
|
||||
private void replayChain(long startTime) throws BlockStoreException {
|
||||
this.replayChain(startTime, null);
|
||||
}
|
||||
|
||||
// Actual useful methods for use by other classes
|
||||
|
||||
/** Returns median timestamp from latest 11 blocks, in seconds. */
|
||||
public Long getMedianBlockTime() {
|
||||
// 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
|
||||
long startTime = (System.currentTimeMillis() / 1000L) - 11 * 60 * 60; // 11 hours before now, in seconds
|
||||
|
||||
try {
|
||||
manager.saveAsText(new File(directory, checkpointsFileName));
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new RuntimeException("Failed to save updated BTC checkpoints", e);
|
||||
replayChain(startTime);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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("BTC blockstore issue: %s", e.getMessage()));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Coin getBalance(String base58Address, long startTime) {
|
||||
// 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);
|
||||
|
||||
// Now that blockchain is up-to-date, return current balance
|
||||
return wallet.getBalance();
|
||||
} catch (BlockStoreException e) {
|
||||
LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage()));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<TransactionOutput> getUnspentOutputs(String base58Address, long startTime) {
|
||||
Wallet wallet = createEmptyWallet();
|
||||
Address address = Address.fromString(this.params, base58Address);
|
||||
wallet.addWatchedAddress(address, startTime);
|
||||
|
||||
try {
|
||||
replayChain(startTime, wallet);
|
||||
|
||||
// Now that blockchain is up-to-date, return outputs
|
||||
return wallet.getWatchedOutputs(true);
|
||||
} catch (BlockStoreException e) {
|
||||
LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage()));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -41,6 +41,12 @@
|
||||
"blockTimingsByHeight": [
|
||||
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||
],
|
||||
"ciyamAtSettings": {
|
||||
"feePerStep": "0.0001",
|
||||
"maxStepsPerRound": 500,
|
||||
"stepsPerFunctionCall": 10,
|
||||
"minutesPerBlock": 1
|
||||
},
|
||||
"featureTriggers": {
|
||||
"messageHeight": 0,
|
||||
"atHeight": 0,
|
||||
|
Reference in New Issue
Block a user