forked from Qortal/qortal
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:
parent
369a45f5c0
commit
5c0134c16a
@ -6,14 +6,15 @@ import java.nio.ByteOrder;
|
|||||||
import org.bitcoinj.core.Address;
|
import org.bitcoinj.core.Address;
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bitcoinj.core.ECKey;
|
import org.bitcoinj.core.ECKey;
|
||||||
import org.bitcoinj.core.InsufficientMoneyException;
|
|
||||||
import org.bitcoinj.core.NetworkParameters;
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
import org.bitcoinj.core.Sha256Hash;
|
|
||||||
import org.bitcoinj.core.Transaction;
|
import org.bitcoinj.core.Transaction;
|
||||||
import org.bitcoinj.core.TransactionOutPoint;
|
import org.bitcoinj.core.Transaction.SigHash;
|
||||||
import org.bitcoinj.script.Script;
|
import org.bitcoinj.core.TransactionInput;
|
||||||
|
import org.bitcoinj.core.TransactionOutput;
|
||||||
|
import org.bitcoinj.crypto.TransactionSignature;
|
||||||
import org.bitcoinj.script.ScriptBuilder;
|
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.bitcoinj.script.Script.ScriptType;
|
||||||
import org.ciyam.at.FunctionCode;
|
import org.ciyam.at.FunctionCode;
|
||||||
import org.ciyam.at.MachineState;
|
import org.ciyam.at.MachineState;
|
||||||
@ -25,6 +26,8 @@ import com.google.common.primitives.Bytes;
|
|||||||
|
|
||||||
public class BTCACCT {
|
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[] 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[] 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)
|
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);
|
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) {
|
public static byte[] buildCiyamAT(byte[] secretHash, byte[] destinationQortalPubKey, long refundMinutes) {
|
||||||
// Labels for data segment addresses
|
// Labels for data segment addresses
|
||||||
int addrCounter = 0;
|
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.GenesisAccount;
|
||||||
import org.qortal.account.PublicKeyAccount;
|
import org.qortal.account.PublicKeyAccount;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
|
import org.qortal.block.BlockChain;
|
||||||
|
import org.qortal.block.BlockChain.CiyamAtSettings;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.at.ATData;
|
import org.qortal.data.at.ATData;
|
||||||
import org.qortal.data.block.BlockData;
|
import org.qortal.data.block.BlockData;
|
||||||
@ -33,16 +35,11 @@ import com.google.common.primitives.Bytes;
|
|||||||
|
|
||||||
public class QortalATAPI extends API {
|
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
|
// Properties
|
||||||
Repository repository;
|
private Repository repository;
|
||||||
ATData atData;
|
private ATData atData;
|
||||||
long blockTimestamp;
|
private long blockTimestamp;
|
||||||
|
private final CiyamAtSettings ciyamAtSettings;
|
||||||
|
|
||||||
/** List of generated AT transactions */
|
/** List of generated AT transactions */
|
||||||
List<AtTransaction> transactions;
|
List<AtTransaction> transactions;
|
||||||
@ -54,36 +51,42 @@ public class QortalATAPI extends API {
|
|||||||
this.atData = atData;
|
this.atData = atData;
|
||||||
this.transactions = new ArrayList<>();
|
this.transactions = new ArrayList<>();
|
||||||
this.blockTimestamp = blockTimestamp;
|
this.blockTimestamp = blockTimestamp;
|
||||||
|
|
||||||
|
this.ciyamAtSettings = BlockChain.getInstance().getCiyamAtSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Methods specific to Qortal AT processing, not inherited
|
// Methods specific to Qortal AT processing, not inherited
|
||||||
|
|
||||||
|
public Repository getRepository() {
|
||||||
|
return this.repository;
|
||||||
|
}
|
||||||
|
|
||||||
public List<AtTransaction> getTransactions() {
|
public List<AtTransaction> getTransactions() {
|
||||||
return this.transactions;
|
return this.transactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BigDecimal calcFinalFees(MachineState state) {
|
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
|
// Inherited methods from CIYAM AT API
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getMaxStepsPerRound() {
|
public int getMaxStepsPerRound() {
|
||||||
return MAX_STEPS_PER_ROUND;
|
return this.ciyamAtSettings.maxStepsPerRound;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getOpCodeSteps(OpCode opcode) {
|
public int getOpCodeSteps(OpCode opcode) {
|
||||||
if (opcode.value >= OpCode.EXT_FUN.value && opcode.value <= OpCode.EXT_FUN_RET_DAT_2.value)
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getFeePerStep() {
|
public long getFeePerStep() {
|
||||||
return FEE_PER_STEP.unscaledValue().longValue();
|
return this.ciyamAtSettings.feePerStep.unscaledValue().longValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -303,7 +306,7 @@ public class QortalATAPI extends API {
|
|||||||
int blockHeight = timestamp.blockHeight;
|
int blockHeight = timestamp.blockHeight;
|
||||||
|
|
||||||
// At least one block in the future
|
// 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();
|
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. */
|
/** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */
|
||||||
private long onlineAccountSignaturesMaxLifetime;
|
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.
|
// Constructors, etc.
|
||||||
|
|
||||||
@ -342,6 +354,10 @@ public class BlockChain {
|
|||||||
return this.onlineAccountSignaturesMaxLifetime;
|
return this.onlineAccountSignaturesMaxLifetime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CiyamAtSettings getCiyamAtSettings() {
|
||||||
|
return this.ciyamAtSettings;
|
||||||
|
}
|
||||||
|
|
||||||
// Convenience methods for specific blockchain feature triggers
|
// Convenience methods for specific blockchain feature triggers
|
||||||
|
|
||||||
public long getMessageReleaseHeight() {
|
public long getMessageReleaseHeight() {
|
||||||
@ -437,6 +453,9 @@ public class BlockChain {
|
|||||||
if (this.founderEffectiveMintingLevel <= 0)
|
if (this.founderEffectiveMintingLevel <= 0)
|
||||||
Settings.throwValidationError("Invalid/missing \"founderEffectiveMintingLevel\" in blockchain config");
|
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)
|
if (this.featureTriggers == null)
|
||||||
Settings.throwValidationError("No \"featureTriggers\" entry found in blockchain config");
|
Settings.throwValidationError("No \"featureTriggers\" entry found in blockchain config");
|
||||||
|
|
||||||
@ -452,6 +471,8 @@ public class BlockChain {
|
|||||||
this.unitFee = this.unitFee.setScale(8);
|
this.unitFee = this.unitFee.setScale(8);
|
||||||
this.minFeePerByte = this.unitFee.divide(this.maxBytesPerUnitFee, MathContext.DECIMAL32);
|
this.minFeePerByte = this.unitFee.divide(this.maxBytesPerUnitFee, MathContext.DECIMAL32);
|
||||||
|
|
||||||
|
this.ciyamAtSettings.feePerStep.setScale(8);
|
||||||
|
|
||||||
// Pre-calculate cumulative blocks required for each level
|
// Pre-calculate cumulative blocks required for each level
|
||||||
int cumulativeBlocks = 0;
|
int cumulativeBlocks = 0;
|
||||||
this.cumulativeBlocksByLevel = new ArrayList<>(this.blocksNeededByLevel.size() + 1);
|
this.cumulativeBlocksByLevel = new ArrayList<>(this.blocksNeededByLevel.size() + 1);
|
||||||
|
@ -15,35 +15,34 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.security.DigestOutputStream;
|
import java.security.DigestOutputStream;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
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.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.Address;
|
||||||
import org.bitcoinj.core.BlockChain;
|
import org.bitcoinj.core.BlockChain;
|
||||||
import org.bitcoinj.core.CheckpointManager;
|
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.PeerGroup;
|
import org.bitcoinj.core.PeerGroup;
|
||||||
import org.bitcoinj.core.Sha256Hash;
|
import org.bitcoinj.core.Sha256Hash;
|
||||||
import org.bitcoinj.core.StoredBlock;
|
import org.bitcoinj.core.StoredBlock;
|
||||||
import org.bitcoinj.core.Transaction;
|
|
||||||
import org.bitcoinj.core.TransactionOutput;
|
import org.bitcoinj.core.TransactionOutput;
|
||||||
import org.bitcoinj.core.VerificationException;
|
|
||||||
import org.bitcoinj.core.listeners.NewBestBlockListener;
|
import org.bitcoinj.core.listeners.NewBestBlockListener;
|
||||||
import org.bitcoinj.net.discovery.DnsDiscovery;
|
import org.bitcoinj.net.discovery.DnsDiscovery;
|
||||||
import org.bitcoinj.params.MainNetParams;
|
import org.bitcoinj.params.MainNetParams;
|
||||||
import org.bitcoinj.params.TestNet3Params;
|
import org.bitcoinj.params.TestNet3Params;
|
||||||
import org.bitcoinj.script.Script;
|
|
||||||
import org.bitcoinj.store.BlockStore;
|
import org.bitcoinj.store.BlockStore;
|
||||||
import org.bitcoinj.store.BlockStoreException;
|
import org.bitcoinj.store.BlockStoreException;
|
||||||
import org.bitcoinj.store.SPVBlockStore;
|
import org.bitcoinj.store.MemoryBlockStore;
|
||||||
import org.bitcoinj.utils.Threading;
|
import org.bitcoinj.utils.Threading;
|
||||||
import org.bitcoinj.wallet.KeyChainGroup;
|
|
||||||
import org.bitcoinj.wallet.Wallet;
|
import org.bitcoinj.wallet.Wallet;
|
||||||
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
|
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
|
||||||
|
import org.bitcoinj.wallet.listeners.WalletCoinsSentEventListener;
|
||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
|
|
||||||
public class BTC {
|
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 BTC instance;
|
||||||
|
|
||||||
private static File directory;
|
private final NetworkParameters params;
|
||||||
private static String chainFileName;
|
private final String checkpointsFileName;
|
||||||
private static String checkpointsFileName;
|
private final File directory;
|
||||||
|
|
||||||
private static NetworkParameters params;
|
private PeerGroup peerGroup;
|
||||||
private static PeerGroup peerGroup;
|
private BlockStore blockStore;
|
||||||
private static BlockStore blockStore;
|
private BlockChain chain;
|
||||||
|
|
||||||
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 static class UpdateableCheckpointManager extends CheckpointManager implements NewBestBlockListener {
|
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 MINIMAL_TESTNET3_TEXTFILE = "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_MAINNET_TEXTFILE = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAABjl7tqvU/FIcDT9gcbVlA4nwtFUbxAtOawZzBpAAAAAKzkcK7NqciBjI/ldojNKncrWleVSgDfBCCn3VRrbSxXaw5/Sf//AB0z8Bkv\n";
|
||||||
|
|
||||||
public UpdateableCheckpointManager(NetworkParameters params) throws IOException {
|
public UpdateableCheckpointManager(NetworkParameters params) throws IOException {
|
||||||
super(params, getMinimalTextFileStream(params));
|
super(params, getMinimalTextFileStream(params));
|
||||||
@ -97,20 +86,35 @@ public class BTC {
|
|||||||
|
|
||||||
private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params) {
|
private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params) {
|
||||||
if (params == MainNetParams.get())
|
if (params == MainNetParams.get())
|
||||||
return new ByteArrayInputStream(minimalMainNetTextFile.getBytes());
|
return new ByteArrayInputStream(MINIMAL_MAINNET_TEXTFILE.getBytes());
|
||||||
|
|
||||||
if (params == TestNet3Params.get())
|
if (params == TestNet3Params.get())
|
||||||
return new ByteArrayInputStream(minimalTestNet3TextFile.getBytes());
|
return new ByteArrayInputStream(MINIMAL_TESTNET3_TEXTFILE.getBytes());
|
||||||
|
|
||||||
throw new RuntimeException("Failed to construct empty UpdateableCheckpointManageer");
|
throw new RuntimeException("Failed to construct empty UpdateableCheckpointManageer");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void notifyNewBestBlock(StoredBlock block) throws VerificationException {
|
public void notifyNewBestBlock(StoredBlock block) {
|
||||||
int height = block.getHeight();
|
final int height = block.getHeight();
|
||||||
|
|
||||||
if (height % checkpointInterval == 0)
|
if (height % this.params.getInterval() != 0)
|
||||||
checkpoints.put(block.getHeader().getTimeSeconds(), block);
|
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 {
|
public void saveAsText(File textFile) throws FileNotFoundException {
|
||||||
@ -118,7 +122,9 @@ public class BTC {
|
|||||||
writer.println("TXT CHECKPOINTS 1");
|
writer.println("TXT CHECKPOINTS 1");
|
||||||
writer.println("0"); // Number of signatures to read. Do this later.
|
writer.println("0"); // Number of signatures to read. Do this later.
|
||||||
writer.println(checkpoints.size());
|
writer.println(checkpoints.size());
|
||||||
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
|
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
|
||||||
|
|
||||||
for (StoredBlock block : checkpoints.values()) {
|
for (StoredBlock block : checkpoints.values()) {
|
||||||
block.serializeCompact(buffer);
|
block.serializeCompact(buffer);
|
||||||
writer.println(CheckpointManager.BASE64.encode(buffer.array()));
|
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.
|
dataOutputStream.writeInt(0); // Number of signatures to read. Do this later.
|
||||||
digestOutputStream.on(true);
|
digestOutputStream.on(true);
|
||||||
dataOutputStream.writeInt(checkpoints.size());
|
dataOutputStream.writeInt(checkpoints.size());
|
||||||
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
|
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
|
||||||
|
|
||||||
for (StoredBlock block : checkpoints.values()) {
|
for (StoredBlock block : checkpoints.values()) {
|
||||||
block.serializeCompact(buffer);
|
block.serializeCompact(buffer);
|
||||||
dataOutputStream.write(buffer.array());
|
dataOutputStream.write(buffer.array());
|
||||||
@ -151,9 +159,37 @@ public class BTC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private static UpdateableCheckpointManager manager;
|
private UpdateableCheckpointManager manager;
|
||||||
|
|
||||||
|
// Constructors and instance
|
||||||
|
|
||||||
private BTC() {
|
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() {
|
public static synchronized BTC getInstance() {
|
||||||
@ -163,160 +199,152 @@ public class BTC {
|
|||||||
return instance;
|
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) {
|
public static byte[] hash160(byte[] message) {
|
||||||
return RIPE_MD160_DIGESTER.digest(SHA256_DIGESTER.digest(message));
|
return RIPE_MD160_DIGESTER.digest(SHA256_DIGESTER.digest(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start() {
|
// Start-up & shutdown
|
||||||
// Start wallet
|
private void start(long startTime) throws BlockStoreException {
|
||||||
if (Settings.getInstance().useBitcoinTestNet()) {
|
StoredBlock checkpoint = this.manager.getCheckpointBefore(startTime - 1);
|
||||||
params = TestNet3Params.get();
|
|
||||||
chainFileName = "bitcoinj-testnet.spvchain";
|
|
||||||
checkpointsFileName = "checkpoints-testnet.txt";
|
|
||||||
} else {
|
|
||||||
params = MainNetParams.get();
|
|
||||||
chainFileName = "bitcoinj.spvchain";
|
|
||||||
checkpointsFileName = "checkpoints.txt";
|
|
||||||
}
|
|
||||||
|
|
||||||
directory = new File("Qortal-BTC");
|
this.blockStore = new MemoryBlockStore(params);
|
||||||
if (!directory.exists())
|
this.blockStore.put(checkpoint);
|
||||||
directory.mkdirs();
|
this.blockStore.setChainHead(checkpoint);
|
||||||
|
|
||||||
File chainFile = new File(directory, chainFileName);
|
this.chain = new BlockChain(this.params, this.blockStore);
|
||||||
|
|
||||||
try {
|
this.peerGroup = new PeerGroup(this.params, this.chain);
|
||||||
blockStore = new SPVBlockStore(params, chainFile);
|
this.peerGroup.setUserAgent("qortal", "1.0");
|
||||||
} catch (BlockStoreException e) {
|
this.peerGroup.addPeerDiscovery(new DnsDiscovery(this.params));
|
||||||
throw new RuntimeException("Failed to open/create BTC SPVBlockStore", e);
|
this.peerGroup.start();
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void shutdown() {
|
private void stop() {
|
||||||
if (instance == null)
|
this.peerGroup.stop();
|
||||||
return;
|
|
||||||
|
|
||||||
instance = null;
|
|
||||||
|
|
||||||
peerGroup.stop();
|
|
||||||
|
|
||||||
try {
|
|
||||||
blockStore.close();
|
|
||||||
} catch (BlockStoreException e) {
|
|
||||||
// What can we do?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Utility methods
|
||||||
|
|
||||||
protected Wallet createEmptyWallet() {
|
protected Wallet createEmptyWallet() {
|
||||||
ECKey dummyKey = new ECKey();
|
return Wallet.createBasic(this.params);
|
||||||
|
|
||||||
KeyChainGroup keyChainGroup = KeyChainGroup.createBasic(params);
|
|
||||||
keyChainGroup.importKeys(dummyKey);
|
|
||||||
|
|
||||||
Wallet wallet = new Wallet(params, keyChainGroup);
|
|
||||||
|
|
||||||
wallet.removeKey(dummyKey);
|
|
||||||
|
|
||||||
return wallet;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void watch(String base58Address, long startTime) throws InterruptedException, ExecutionException, TimeoutException, BlockStoreException {
|
private void replayChain(long startTime, Wallet wallet) throws BlockStoreException {
|
||||||
Wallet wallet = createEmptyWallet();
|
this.start(startTime);
|
||||||
|
|
||||||
WalletCoinsReceivedEventListener coinsReceivedListener = new WalletCoinsReceivedEventListener() {
|
final WalletCoinsReceivedEventListener coinsReceivedListener = (someWallet, tx, prevBalance, newBalance) -> {
|
||||||
@Override
|
LOGGER.debug(String.format("Wallet-related transaction %s", tx.getTxId()));
|
||||||
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
|
|
||||||
System.out.println("Coins received via transaction " + tx.getTxId().toString());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
wallet.addCoinsReceivedEventListener(coinsReceivedListener);
|
|
||||||
|
|
||||||
Address address = Address.fromString(params, base58Address);
|
final WalletCoinsSentEventListener coinsSentListener = (someWallet, tx, prevBalance, newBalance) -> {
|
||||||
wallet.addWatchedAddress(address, startTime);
|
LOGGER.debug(String.format("Wallet-related transaction %s", tx.getTxId()));
|
||||||
|
};
|
||||||
|
|
||||||
StoredBlock checkpoint = manager.getCheckpointBefore(startTime);
|
if (wallet != null) {
|
||||||
blockStore.put(checkpoint);
|
wallet.addCoinsReceivedEventListener(coinsReceivedListener);
|
||||||
blockStore.setChainHead(checkpoint);
|
wallet.addCoinsSentEventListener(coinsSentListener);
|
||||||
chain.setChainHead(checkpoint);
|
|
||||||
|
|
||||||
chain.addWallet(wallet);
|
// Link wallet to chain and peerGroup
|
||||||
peerGroup.addWallet(wallet);
|
this.chain.addWallet(wallet);
|
||||||
peerGroup.setFastCatchupTimeSecs(startTime);
|
this.peerGroup.addWallet(wallet);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) -> {
|
this.stop();
|
||||||
if (blocksLeft % 1000 == 0)
|
}
|
||||||
System.out.println("Blocks left: " + blocksLeft);
|
}
|
||||||
});
|
|
||||||
|
|
||||||
System.out.println("Starting download...");
|
private void replayChain(long startTime) throws BlockStoreException {
|
||||||
peerGroup.downloadBlockChain();
|
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 {
|
try {
|
||||||
manager.saveAsText(new File(directory, checkpointsFileName));
|
replayChain(startTime);
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
throw new RuntimeException("Failed to save updated BTC checkpoints", e);
|
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": [
|
"blockTimingsByHeight": [
|
||||||
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
{ "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 }
|
||||||
],
|
],
|
||||||
|
"ciyamAtSettings": {
|
||||||
|
"feePerStep": "0.0001",
|
||||||
|
"maxStepsPerRound": 500,
|
||||||
|
"stepsPerFunctionCall": 10,
|
||||||
|
"minutesPerBlock": 1
|
||||||
|
},
|
||||||
"featureTriggers": {
|
"featureTriggers": {
|
||||||
"messageHeight": 0,
|
"messageHeight": 0,
|
||||||
"atHeight": 0,
|
"atHeight": 0,
|
||||||
|
@ -1,30 +1,23 @@
|
|||||||
package org.qora.test.btcacct;
|
package org.qora.test.btcacct;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.util.concurrent.CancellationException;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
|
|
||||||
import org.bitcoinj.core.Address;
|
import org.bitcoinj.core.Address;
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bitcoinj.core.ECKey;
|
import org.bitcoinj.core.ECKey;
|
||||||
import org.bitcoinj.core.InsufficientMoneyException;
|
|
||||||
import org.bitcoinj.core.LegacyAddress;
|
import org.bitcoinj.core.LegacyAddress;
|
||||||
import org.bitcoinj.core.NetworkParameters;
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
import org.bitcoinj.core.Transaction;
|
|
||||||
import org.bitcoinj.core.TransactionBroadcast;
|
|
||||||
import org.bitcoinj.kits.WalletAppKit;
|
|
||||||
import org.bitcoinj.params.TestNet3Params;
|
import org.bitcoinj.params.TestNet3Params;
|
||||||
import org.bitcoinj.script.Script.ScriptType;
|
import org.bitcoinj.script.Script.ScriptType;
|
||||||
import org.bitcoinj.wallet.SendRequest;
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.qora.account.PrivateKeyAccount;
|
import org.qora.account.PrivateKeyAccount;
|
||||||
import org.qora.account.PublicKeyAccount;
|
import org.qora.account.PublicKeyAccount;
|
||||||
|
import org.qora.asset.Asset;
|
||||||
import org.qora.controller.Controller;
|
import org.qora.controller.Controller;
|
||||||
import org.qora.crosschain.BTC;
|
import org.qora.crosschain.BTC;
|
||||||
import org.qora.crosschain.BTCACCT;
|
import org.qora.crosschain.BTCACCT;
|
||||||
@ -99,13 +92,12 @@ public class Initiate1 {
|
|||||||
|
|
||||||
byte[] yourQortPrivKey = Base58.decode(yourQortPrivKey58);
|
byte[] yourQortPrivKey = Base58.decode(yourQortPrivKey58);
|
||||||
PrivateKeyAccount yourQortalAccount = new PrivateKeyAccount(repository, yourQortPrivKey);
|
PrivateKeyAccount yourQortalAccount = new PrivateKeyAccount(repository, yourQortPrivKey);
|
||||||
byte[] yourQortPubKey = yourQortalAccount.getPublicKey();
|
|
||||||
System.out.println(String.format("Your Qortal address: %s", yourQortalAccount.getAddress()));
|
System.out.println(String.format("Your Qortal address: %s", yourQortalAccount.getAddress()));
|
||||||
|
|
||||||
byte[] yourBitcoinPubKey = HashCode.fromString(yourBitcoinPubKeyHex).asBytes();
|
byte[] yourBitcoinPubKey = HashCode.fromString(yourBitcoinPubKeyHex).asBytes();
|
||||||
ECKey yourBitcoinKey = ECKey.fromPublicOnly(yourBitcoinPubKey);
|
ECKey yourBitcoinKey = ECKey.fromPublicOnly(yourBitcoinPubKey);
|
||||||
Address yourBitcoinAddress = Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH);
|
Address yourBitcoinAddress = Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH);
|
||||||
System.out.println(String.format("Your Bitcoin address: %s", yourBitcoinAddress.toString()));
|
System.out.println(String.format("Your Bitcoin address: %s", yourBitcoinAddress));
|
||||||
|
|
||||||
byte[] theirQortPubKey = Base58.decode(theirQortPubKey58);
|
byte[] theirQortPubKey = Base58.decode(theirQortPubKey58);
|
||||||
PublicKeyAccount theirQortalAccount = new PublicKeyAccount(repository, theirQortPubKey);
|
PublicKeyAccount theirQortalAccount = new PublicKeyAccount(repository, theirQortPubKey);
|
||||||
@ -114,7 +106,15 @@ public class Initiate1 {
|
|||||||
byte[] theirBitcoinPubKey = HashCode.fromString(theirBitcoinPubKeyHex).asBytes();
|
byte[] theirBitcoinPubKey = HashCode.fromString(theirBitcoinPubKeyHex).asBytes();
|
||||||
ECKey theirBitcoinKey = ECKey.fromPublicOnly(theirBitcoinPubKey);
|
ECKey theirBitcoinKey = ECKey.fromPublicOnly(theirBitcoinPubKey);
|
||||||
Address theirBitcoinAddress = Address.fromKey(params, theirBitcoinKey, ScriptType.P2PKH);
|
Address theirBitcoinAddress = Address.fromKey(params, theirBitcoinKey, ScriptType.P2PKH);
|
||||||
System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress.toString()));
|
System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress));
|
||||||
|
|
||||||
|
// Some checks
|
||||||
|
BigDecimal qortAmount = new BigDecimal(rawQortAmount).setScale(8);
|
||||||
|
BigDecimal yourQortBalance = yourQortalAccount.getConfirmedBalance(Asset.QORT);
|
||||||
|
if (yourQortBalance.compareTo(qortAmount) <= 0) {
|
||||||
|
System.err.println(String.format("Your QORT balance %s is less than required %s", yourQortBalance.toPlainString(), qortAmount.toPlainString()));
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
// New/derived info
|
// New/derived info
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ public class Initiate1 {
|
|||||||
System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString());
|
System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString());
|
||||||
|
|
||||||
int lockTime = (int) ((System.currentTimeMillis() / 1000L) + REFUND_TIMEOUT);
|
int lockTime = (int) ((System.currentTimeMillis() / 1000L) + REFUND_TIMEOUT);
|
||||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()).toString(), lockTime));
|
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()), lockTime));
|
||||||
|
|
||||||
byte[] redeemScriptBytes = BTCACCT.buildRedeemScript(secretHash, yourBitcoinPubKey, theirBitcoinPubKey, lockTime);
|
byte[] redeemScriptBytes = BTCACCT.buildRedeemScript(secretHash, yourBitcoinPubKey, theirBitcoinPubKey, lockTime);
|
||||||
System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString());
|
System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString());
|
||||||
@ -139,10 +139,10 @@ public class Initiate1 {
|
|||||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
System.out.println("P2SH address: " + p2shAddress.toString());
|
System.out.println("P2SH address: " + p2shAddress.toString());
|
||||||
|
|
||||||
Coin bitcoinAmount = Coin.parseCoin(rawBitcoinAmount);
|
Coin bitcoinAmount = Coin.parseCoin(rawBitcoinAmount).add(BTCACCT.DEFAULT_BTC_FEE);
|
||||||
|
|
||||||
// Fund P2SH
|
// Fund P2SH
|
||||||
System.out.println(String.format("\nYou need to fund %s with %s BTC", p2shAddress.toString(), bitcoinAmount.toPlainString()));
|
System.out.println(String.format("\nYou need to fund %s with %s BTC (includes redeem/refund fee)", p2shAddress.toString(), bitcoinAmount.toPlainString()));
|
||||||
|
|
||||||
System.out.println("Once this is done, responder should run Respond2 to check P2SH funding and create AT");
|
System.out.println("Once this is done, responder should run Respond2 to check P2SH funding and create AT");
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
|
174
src/test/java/org/qora/test/btcacct/Refund2.java
Normal file
174
src/test/java/org/qora/test/btcacct/Refund2.java
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package org.qora.test.btcacct;
|
||||||
|
|
||||||
|
import java.security.Security;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.bitcoinj.core.Address;
|
||||||
|
import org.bitcoinj.core.Coin;
|
||||||
|
import org.bitcoinj.core.ECKey;
|
||||||
|
import org.bitcoinj.core.LegacyAddress;
|
||||||
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
|
import org.bitcoinj.core.Transaction;
|
||||||
|
import org.bitcoinj.core.TransactionOutput;
|
||||||
|
import org.bitcoinj.params.TestNet3Params;
|
||||||
|
import org.bitcoinj.script.Script.ScriptType;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.qora.controller.Controller;
|
||||||
|
import org.qora.crosschain.BTC;
|
||||||
|
import org.qora.crosschain.BTCACCT;
|
||||||
|
import org.qora.repository.DataException;
|
||||||
|
import org.qora.repository.Repository;
|
||||||
|
import org.qora.repository.RepositoryFactory;
|
||||||
|
import org.qora.repository.RepositoryManager;
|
||||||
|
import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||||
|
import org.qora.settings.Settings;
|
||||||
|
|
||||||
|
import com.google.common.hash.HashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiator must be Qora-chain so that initiator can send initial message to BTC P2SH then Qora can scan for P2SH add send corresponding message to Qora AT.
|
||||||
|
*
|
||||||
|
* Initiator (wants Qora, has BTC)
|
||||||
|
* Funds BTC P2SH address
|
||||||
|
*
|
||||||
|
* Responder (has Qora, wants BTC)
|
||||||
|
* Builds Qora ACCT AT and funds it with Qora
|
||||||
|
*
|
||||||
|
* Initiator sends recipient+secret+script as input to BTC P2SH address, releasing BTC amount - fees to responder
|
||||||
|
*
|
||||||
|
* Qora nodes scan for P2SH output, checks amount and recipient and if ok sends secret to Qora ACCT AT
|
||||||
|
* (Or it's possible to feed BTC transaction details into Qora AT so it can check them itself?)
|
||||||
|
*
|
||||||
|
* Qora ACCT AT sends its Qora to initiator
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class Refund2 {
|
||||||
|
|
||||||
|
static {
|
||||||
|
// This must go before any calls to LogManager/Logger
|
||||||
|
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final long REFUND_TIMEOUT = 600L; // seconds
|
||||||
|
|
||||||
|
private static void usage() {
|
||||||
|
System.err.println(String.format("usage: Refund2 <your-BTC-PRIVkey> <their-BTC-pubkey> <hash-of-secret> <locktime> <P2SH-address>"));
|
||||||
|
System.err.println(String.format("example: Refund2 027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c \\\n"
|
||||||
|
+ "\t032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n"
|
||||||
|
+ "\tb837056cdc5d805e4db1f830a58158e1131ac96ea71de4c6f9d7854985e153e2 1575021641 2MvGdGUgAfc7qTHaZJwWmZ26Fg6Hjif8gNy"));
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
if (args.length != 5)
|
||||||
|
usage();
|
||||||
|
|
||||||
|
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||||
|
|
||||||
|
Settings.fileInstance("settings-test.json");
|
||||||
|
|
||||||
|
NetworkParameters params = TestNet3Params.get();
|
||||||
|
|
||||||
|
int argIndex = 0;
|
||||||
|
String yourBitcoinPrivKeyHex = args[argIndex++];
|
||||||
|
String theirBitcoinPubKeyHex = args[argIndex++];
|
||||||
|
|
||||||
|
String secretHashHex = args[argIndex++];
|
||||||
|
String rawLockTime = args[argIndex++];
|
||||||
|
String rawP2shAddress = args[argIndex++];
|
||||||
|
|
||||||
|
try {
|
||||||
|
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl());
|
||||||
|
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw new RuntimeException("Repository startup issue: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
System.out.println("Confirm the following is correct based on the info you've given:");
|
||||||
|
|
||||||
|
byte[] yourBitcoinPrivKey = HashCode.fromString(yourBitcoinPrivKeyHex).asBytes();
|
||||||
|
ECKey yourBitcoinKey = ECKey.fromPrivate(yourBitcoinPrivKey);
|
||||||
|
Address yourBitcoinAddress = Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH);
|
||||||
|
System.out.println(String.format("Your Bitcoin address: %s", yourBitcoinAddress));
|
||||||
|
|
||||||
|
byte[] theirBitcoinPubKey = HashCode.fromString(theirBitcoinPubKeyHex).asBytes();
|
||||||
|
ECKey theirBitcoinKey = ECKey.fromPublicOnly(theirBitcoinPubKey);
|
||||||
|
Address theirBitcoinAddress = Address.fromKey(params, theirBitcoinKey, ScriptType.P2PKH);
|
||||||
|
System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress));
|
||||||
|
|
||||||
|
// New/derived info
|
||||||
|
|
||||||
|
int lockTime = Integer.valueOf(rawLockTime);
|
||||||
|
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()), lockTime));
|
||||||
|
|
||||||
|
byte[] secretHash = HashCode.fromString(secretHashHex).asBytes();
|
||||||
|
System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString());
|
||||||
|
|
||||||
|
byte[] redeemScriptBytes = BTCACCT.buildRedeemScript(secretHash, yourBitcoinKey.getPubKey(), theirBitcoinPubKey, lockTime);
|
||||||
|
System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString());
|
||||||
|
|
||||||
|
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
|
||||||
|
|
||||||
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
|
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||||
|
|
||||||
|
if (!p2shAddress.toString().equals(rawP2shAddress)) {
|
||||||
|
System.err.println(String.format("Derived P2SH address %s does not match given address %s", p2shAddress, rawP2shAddress));
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some checks
|
||||||
|
long medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||||
|
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneId.systemDefault())));
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
if (now < medianBlockTime * 1000L) {
|
||||||
|
System.err.println(String.format("Too soon (%s) to refund based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.systemDefault()), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneId.systemDefault())));
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now < lockTime * 1000L) {
|
||||||
|
System.err.println(String.format("Too soon (%s) to refund based on lockTime %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneId.systemDefault()), LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault())));
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), lockTime - REFUND_TIMEOUT);
|
||||||
|
if (p2shBalance == null) {
|
||||||
|
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
|
||||||
|
|
||||||
|
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||||
|
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString(), lockTime - REFUND_TIMEOUT);
|
||||||
|
System.out.println(String.format("Found %d unspent output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||||
|
|
||||||
|
if (fundingOutputs.isEmpty()) {
|
||||||
|
System.err.println(String.format("Can't refund spent/unfunded P2SH"));
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fundingOutputs.size() != 1) {
|
||||||
|
System.err.println(String.format("Expecting only one unspent output for P2SH"));
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction refundTransaction = BTCACCT.buildRefundTransaction(p2shBalance, yourBitcoinKey, fundingOutputs.get(0), redeemScriptBytes, lockTime);
|
||||||
|
|
||||||
|
byte[] refundBytes = refundTransaction.bitcoinSerialize();
|
||||||
|
|
||||||
|
System.out.println(String.format("\nLoad this transaction into your wallet, sign and broadcast:\n%s\n", HashCode.fromBytes(refundBytes).toString()));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
usage();
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw new RuntimeException("Repository issue: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
package org.qora.test.btcacct;
|
package org.qora.test.btcacct;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
|
|
||||||
import org.bitcoinj.core.Address;
|
import org.bitcoinj.core.Address;
|
||||||
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bitcoinj.core.ECKey;
|
import org.bitcoinj.core.ECKey;
|
||||||
import org.bitcoinj.core.LegacyAddress;
|
import org.bitcoinj.core.LegacyAddress;
|
||||||
import org.bitcoinj.core.NetworkParameters;
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
@ -20,7 +20,6 @@ import org.qora.asset.Asset;
|
|||||||
import org.qora.controller.Controller;
|
import org.qora.controller.Controller;
|
||||||
import org.qora.crosschain.BTC;
|
import org.qora.crosschain.BTC;
|
||||||
import org.qora.crosschain.BTCACCT;
|
import org.qora.crosschain.BTCACCT;
|
||||||
import org.qora.crypto.Crypto;
|
|
||||||
import org.qora.data.transaction.BaseTransactionData;
|
import org.qora.data.transaction.BaseTransactionData;
|
||||||
import org.qora.data.transaction.DeployAtTransactionData;
|
import org.qora.data.transaction.DeployAtTransactionData;
|
||||||
import org.qora.data.transaction.TransactionData;
|
import org.qora.data.transaction.TransactionData;
|
||||||
@ -30,8 +29,11 @@ import org.qora.repository.Repository;
|
|||||||
import org.qora.repository.RepositoryFactory;
|
import org.qora.repository.RepositoryFactory;
|
||||||
import org.qora.repository.RepositoryManager;
|
import org.qora.repository.RepositoryManager;
|
||||||
import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
|
import org.qora.repository.hsqldb.HSQLDBRepositoryFactory;
|
||||||
|
import org.qora.settings.Settings;
|
||||||
import org.qora.transaction.DeployAtTransaction;
|
import org.qora.transaction.DeployAtTransaction;
|
||||||
import org.qora.transaction.Transaction;
|
import org.qora.transaction.Transaction;
|
||||||
|
import org.qora.transform.TransformationException;
|
||||||
|
import org.qora.transform.transaction.TransactionTransformer;
|
||||||
import org.qora.utils.Base58;
|
import org.qora.utils.Base58;
|
||||||
|
|
||||||
import com.google.common.hash.HashCode;
|
import com.google.common.hash.HashCode;
|
||||||
@ -60,12 +62,12 @@ public class Respond2 {
|
|||||||
|
|
||||||
private static void usage() {
|
private static void usage() {
|
||||||
System.err.println(String.format("usage: Respond2 <your-QORT-PRIVkey> <your-BTC-pubkey> <QORT-amount> <BTC-amount> <their-QORT-pubkey> <their-BTC-pubkey> <hash-of-secret> <locktime> <P2SH-address>"));
|
System.err.println(String.format("usage: Respond2 <your-QORT-PRIVkey> <your-BTC-pubkey> <QORT-amount> <BTC-amount> <their-QORT-pubkey> <their-BTC-pubkey> <hash-of-secret> <locktime> <P2SH-address>"));
|
||||||
System.err.println(String.format("example: Respond2 pYQ6DpQBJ2n72TCLJLScEvwhf3boxWy2kQEPynakwpj \\\n"
|
System.err.println(String.format("example: Respond2 3jjoToDaDpsdUHqaouLGypFeewNVKvtkmdM38i54WVra \\\n"
|
||||||
+ "\t03aa20871c2195361f2826c7a649eab6b42639630c4d8c33c55311d5c1e476b5d6 \\\n"
|
|
||||||
+ "\t123 0.00008642 \\\n"
|
|
||||||
+ "\tJBNBQQDzZsm5do1BrwWAp53Ps4KYJVt749EGpCf7ofte \\\n"
|
|
||||||
+ "\t032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n"
|
+ "\t032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n"
|
||||||
+ "\te43f5ab106b70df2e85656de30e1862891752f81e82f5dfd03abb8465a7346f9 1574441679 2N4R2pSEzLcJgtgAbFuLvviwwEkBrmq6sx4"));
|
+ "\t123 0.00008642 \\\n"
|
||||||
|
+ "\t6rNn9b3pYRrG9UKqzMWYZ9qa8F3Zgv2mVWrULGHUusb \\\n"
|
||||||
|
+ "\t03aa20871c2195361f2826c7a649eab6b42639630c4d8c33c55311d5c1e476b5d6 \\\n"
|
||||||
|
+ "\tb837056cdc5d805e4db1f830a58158e1131ac96ea71de4c6f9d7854985e153e2 1575021641 2MvGdGUgAfc7qTHaZJwWmZ26Fg6Hjif8gNy"));
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,6 +76,9 @@ public class Respond2 {
|
|||||||
usage();
|
usage();
|
||||||
|
|
||||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||||
|
|
||||||
|
Settings.fileInstance("settings-test.json");
|
||||||
|
|
||||||
NetworkParameters params = TestNet3Params.get();
|
NetworkParameters params = TestNet3Params.get();
|
||||||
|
|
||||||
int argIndex = 0;
|
int argIndex = 0;
|
||||||
@ -108,7 +113,7 @@ public class Respond2 {
|
|||||||
byte[] yourBitcoinPubKey = HashCode.fromString(yourBitcoinPubKeyHex).asBytes();
|
byte[] yourBitcoinPubKey = HashCode.fromString(yourBitcoinPubKeyHex).asBytes();
|
||||||
ECKey yourBitcoinKey = ECKey.fromPublicOnly(yourBitcoinPubKey);
|
ECKey yourBitcoinKey = ECKey.fromPublicOnly(yourBitcoinPubKey);
|
||||||
Address yourBitcoinAddress = Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH);
|
Address yourBitcoinAddress = Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH);
|
||||||
System.out.println(String.format("Your Bitcoin address: %s", yourBitcoinAddress.toString()));
|
System.out.println(String.format("Your Bitcoin address: %s", yourBitcoinAddress));
|
||||||
|
|
||||||
byte[] theirQortPubKey = Base58.decode(theirQortPubKey58);
|
byte[] theirQortPubKey = Base58.decode(theirQortPubKey58);
|
||||||
PublicKeyAccount theirQortalAccount = new PublicKeyAccount(repository, theirQortPubKey);
|
PublicKeyAccount theirQortalAccount = new PublicKeyAccount(repository, theirQortPubKey);
|
||||||
@ -117,7 +122,7 @@ public class Respond2 {
|
|||||||
byte[] theirBitcoinPubKey = HashCode.fromString(theirBitcoinPubKeyHex).asBytes();
|
byte[] theirBitcoinPubKey = HashCode.fromString(theirBitcoinPubKeyHex).asBytes();
|
||||||
ECKey theirBitcoinKey = ECKey.fromPublicOnly(theirBitcoinPubKey);
|
ECKey theirBitcoinKey = ECKey.fromPublicOnly(theirBitcoinPubKey);
|
||||||
Address theirBitcoinAddress = Address.fromKey(params, theirBitcoinKey, ScriptType.P2PKH);
|
Address theirBitcoinAddress = Address.fromKey(params, theirBitcoinKey, ScriptType.P2PKH);
|
||||||
System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress.toString()));
|
System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress));
|
||||||
|
|
||||||
System.out.println("Hash of secret: " + secretHashHex);
|
System.out.println("Hash of secret: " + secretHashHex);
|
||||||
|
|
||||||
@ -126,7 +131,7 @@ public class Respond2 {
|
|||||||
System.out.println("\nCHECKING info from other party:");
|
System.out.println("\nCHECKING info from other party:");
|
||||||
|
|
||||||
int lockTime = Integer.valueOf(rawLockTime);
|
int lockTime = Integer.valueOf(rawLockTime);
|
||||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()).toString(), lockTime));
|
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()), lockTime));
|
||||||
|
|
||||||
byte[] secretHash = HashCode.fromString(secretHashHex).asBytes();
|
byte[] secretHash = HashCode.fromString(secretHashHex).asBytes();
|
||||||
System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString());
|
System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString());
|
||||||
@ -137,15 +142,26 @@ public class Respond2 {
|
|||||||
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
|
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
|
||||||
|
|
||||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||||
System.out.println("P2SH address: " + p2shAddress.toString());
|
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||||
|
|
||||||
if (!p2shAddress.toString().equals(rawP2shAddress)) {
|
if (!p2shAddress.toString().equals(rawP2shAddress)) {
|
||||||
System.err.println(String.format("Derived P2SH address %s does not match given address %s", p2shAddress.toString(), rawP2shAddress));
|
System.err.println(String.format("Derived P2SH address %s does not match given address %s", p2shAddress, rawP2shAddress));
|
||||||
System.exit(2);
|
System.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Check for funded P2SH
|
// Check for funded P2SH
|
||||||
|
Coin bitcoinAmount = Coin.parseCoin(rawBitcoinAmount).add(BTCACCT.DEFAULT_BTC_FEE);
|
||||||
|
|
||||||
|
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), lockTime - REFUND_TIMEOUT);
|
||||||
|
if (p2shBalance == null) {
|
||||||
|
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
if (p2shBalance.isLessThan(bitcoinAmount)) {
|
||||||
|
System.err.println(String.format("P2SH address %s has lower balance than expected %s BTC", p2shAddress, p2shBalance.toPlainString()));
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString()));
|
||||||
|
|
||||||
System.out.println("\nYour response:");
|
System.out.println("\nYour response:");
|
||||||
|
|
||||||
@ -173,6 +189,16 @@ public class Respond2 {
|
|||||||
deployAtTransactionData.setFee(fee);
|
deployAtTransactionData.setFee(fee);
|
||||||
|
|
||||||
deployAtTransaction.sign(yourQortalAccount);
|
deployAtTransaction.sign(yourQortalAccount);
|
||||||
|
|
||||||
|
byte[] signedBytes = null;
|
||||||
|
try {
|
||||||
|
signedBytes = TransactionTransformer.toBytes(deployAtTransactionData);
|
||||||
|
} catch (TransformationException e) {
|
||||||
|
System.err.println(String.format("Unable to convert transaction to base58: %s", e.getMessage()));
|
||||||
|
System.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println(String.format("\nSigned transaction in base58, ready for POST /transactions/process:\n%s\n", Base58.encode(signedBytes)));
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
usage();
|
usage();
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
|
Loading…
Reference in New Issue
Block a user