Browse Source

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
split-DB
catbref 5 years ago
parent
commit
5c0134c16a
  1. 55
      src/main/java/org/qora/crosschain/BTCACCT.java
  2. 134
      src/main/java/org/qortal/at/BlockchainAPI.java
  3. 31
      src/main/java/org/qortal/at/QortalATAPI.java
  4. 21
      src/main/java/org/qortal/block/BlockChain.java
  5. 320
      src/main/java/org/qortal/crosschain/BTC.java
  6. 6
      src/main/resources/blockchain.json
  7. 28
      src/test/java/org/qora/test/btcacct/Initiate1.java
  8. 174
      src/test/java/org/qora/test/btcacct/Refund2.java
  9. 52
      src/test/java/org/qora/test/btcacct/Respond2.java

55
src/main/java/org/qora/crosschain/BTCACCT.java

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

134
src/main/java/org/qortal/at/BlockchainAPI.java

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

31
src/main/java/org/qortal/at/QortalATAPI.java

@ -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();
}

21
src/main/java/org/qortal/block/BlockChain.java

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

320
src/main/java/org/qortal/crosschain/BTC.java

@ -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 {
}
}
private static BTC instance;
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
private static File directory;
private static String chainFileName;
private static String checkpointsFileName;
private static BTC instance;
private static NetworkParameters params;
private static PeerGroup peerGroup;
private static BlockStore blockStore;
private final NetworkParameters params;
private final String checkpointsFileName;
private final File directory;
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 % 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
if (height % checkpointInterval == 0)
checkpoints.put(block.getHeader().getTimeSeconds(), block);
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,172 +159,192 @@ public class BTC {
}
}
}
private static UpdateableCheckpointManager manager;
private BTC() {
}
public static synchronized BTC getInstance() {
if (instance == null)
instance = new BTC();
return instance;
}
private UpdateableCheckpointManager manager;
public static byte[] hash160(byte[] message) {
return RIPE_MD160_DIGESTER.digest(SHA256_DIGESTER.digest(message));
}
// Constructors and instance
public void start() {
// Start wallet
private BTC() {
if (Settings.getInstance().useBitcoinTestNet()) {
params = TestNet3Params.get();
chainFileName = "bitcoinj-testnet.spvchain";
checkpointsFileName = "checkpoints-testnet.txt";
this.params = TestNet3Params.get();
this.checkpointsFileName = "checkpoints-testnet.txt";
} else {
params = MainNetParams.get();
chainFileName = "bitcoinj.spvchain";
checkpointsFileName = "checkpoints.txt";
this.params = MainNetParams.get();
this.checkpointsFileName = "checkpoints.txt";
}
directory = new File("Qortal-BTC");
if (!directory.exists())
directory.mkdirs();
this.directory = new File("Qortal-BTC");
File chainFile = new File(directory, chainFileName);
if (!this.directory.exists())
this.directory.mkdirs();
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);
File checkpointsFile = new File(this.directory, this.checkpointsFileName);
try (InputStream checkpointsStream = new FileInputStream(checkpointsFile)) {
manager = new UpdateableCheckpointManager(params, checkpointsStream);
this.manager = new UpdateableCheckpointManager(this.params, checkpointsStream);
} catch (FileNotFoundException e) {
// Construct with no checkpoints then
try {
manager = new UpdateableCheckpointManager(params);
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);
}
}
try {
chain = new RollbackBlockChain(params, blockStore);
} catch (BlockStoreException e) {
throw new RuntimeException("Failed to construct BTC blockchain", e);
}
public static synchronized BTC getInstance() {
if (instance == null)
instance = new BTC();
peerGroup = new PeerGroup(params, chain);
peerGroup.setUserAgent("qortal", "1.0");
peerGroup.addPeerDiscovery(new DnsDiscovery(params));
peerGroup.start();
return instance;
}
public synchronized void shutdown() {
if (instance == null)
return;
// Getters & setters
instance = null;
/* package */ File getDirectory() {
return this.directory;
}
peerGroup.stop();
/* package */ String getCheckpointsFileName() {
return this.checkpointsFileName;
}
try {
blockStore.close();
} catch (BlockStoreException e) {
// What can we do?
}
/* package */ NetworkParameters getNetworkParameters() {
return this.params;
}
protected Wallet createEmptyWallet() {
ECKey dummyKey = new ECKey();
// Static utility methods
public static byte[] hash160(byte[] message) {
return RIPE_MD160_DIGESTER.digest(SHA256_DIGESTER.digest(message));
}
KeyChainGroup keyChainGroup = KeyChainGroup.createBasic(params);
keyChainGroup.importKeys(dummyKey);
// Start-up & shutdown
private void start(long startTime) throws BlockStoreException {
StoredBlock checkpoint = this.manager.getCheckpointBefore(startTime - 1);
Wallet wallet = new Wallet(params, keyChainGroup);
this.blockStore = new MemoryBlockStore(params);
this.blockStore.put(checkpoint);
this.blockStore.setChainHead(checkpoint);
wallet.removeKey(dummyKey);
this.chain = new BlockChain(this.params, this.blockStore);
return wallet;
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 void watch(String base58Address, long startTime) throws InterruptedException, ExecutionException, TimeoutException, BlockStoreException {
Wallet wallet = createEmptyWallet();
private void stop() {
this.peerGroup.stop();
}
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());
}
};
wallet.addCoinsReceivedEventListener(coinsReceivedListener);
// Utility methods
Address address = Address.fromString(params, base58Address);
wallet.addWatchedAddress(address, startTime);
protected Wallet createEmptyWallet() {
return Wallet.createBasic(this.params);
}
StoredBlock checkpoint = manager.getCheckpointBefore(startTime);
blockStore.put(checkpoint);
blockStore.setChainHead(checkpoint);
chain.setChainHead(checkpoint);
private void replayChain(long startTime, Wallet wallet) throws BlockStoreException {
this.start(startTime);
chain.addWallet(wallet);
peerGroup.addWallet(wallet);
peerGroup.setFastCatchupTimeSecs(startTime);
final WalletCoinsReceivedEventListener coinsReceivedListener = (someWallet, tx, prevBalance, newBalance) -> {
LOGGER.debug(String.format("Wallet-related transaction %s", tx.getTxId()));
};
peerGroup.addBlocksDownloadedEventListener((peer, block, filteredBlock, blocksLeft) -> {
if (blocksLeft % 1000 == 0)
System.out.println("Blocks left: " + blocksLeft);
});
final WalletCoinsSentEventListener coinsSentListener = (someWallet, tx, prevBalance, newBalance) -> {
LOGGER.debug(String.format("Wallet-related transaction %s", tx.getTxId()));
};
System.out.println("Starting download...");
peerGroup.downloadBlockChain();
if (wallet != null) {
wallet.addCoinsReceivedEventListener(coinsReceivedListener);
wallet.addCoinsSentEventListener(coinsSentListener);
List<TransactionOutput> outputs = wallet.getWatchedOutputs(true);
// Link wallet to chain and peerGroup
this.chain.addWallet(wallet);
this.peerGroup.addWallet(wallet);
}
peerGroup.removeWallet(wallet);
chain.removeWallet(wallet);
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);
this.peerGroup.removeWallet(wallet);
this.chain.removeWallet(wallet);
}
for (TransactionOutput output : outputs)
System.out.println(output.toString());
this.stop();
}
}
public void watch(Script script) {
// wallet.addWatchedScripts(scripts);
private void replayChain(long startTime) throws BlockStoreException {
this.replayChain(startTime, null);
}
public void updateCheckpoints() {
final long now = new Date().getTime() / 1000 - 86400;
// 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 {
StoredBlock checkpoint = manager.getCheckpointBefore(now);
blockStore.put(checkpoint);
blockStore.setChainHead(checkpoint);
chain.setChainHead(checkpoint);
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) {
throw new RuntimeException("Failed to update BTC checkpoints", e);
LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage()));
return null;
}
}
peerGroup.setFastCatchupTimeSecs(now);
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);
chain.addNewBestBlockListener(Threading.SAME_THREAD, manager);
try {
replayChain(startTime, wallet);
peerGroup.addBlocksDownloadedEventListener((peer, block, filteredBlock, blocksLeft) -> {
if (blocksLeft % 1000 == 0)
System.out.println("Blocks left: " + blocksLeft);
});
// 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;
}
}
System.out.println("Starting download...");
peerGroup.downloadBlockChain();
public List<TransactionOutput> getUnspentOutputs(String base58Address, long startTime) {
Wallet wallet = createEmptyWallet();
Address address = Address.fromString(this.params, base58Address);
wallet.addWatchedAddress(address, startTime);
try {
manager.saveAsText(new File(directory, checkpointsFileName));
} catch (FileNotFoundException e) {
throw new RuntimeException("Failed to save updated BTC checkpoints", e);
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;
}
}

6
src/main/resources/blockchain.json

@ -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,

28
src/test/java/org/qora/test/btcacct/Initiate1.java

@ -1,30 +1,23 @@
package org.qora.test.btcacct;
import java.io.File;
import java.math.BigDecimal;
import java.security.SecureRandom;
import java.security.Security;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.LegacyAddress;
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.script.Script.ScriptType;
import org.bitcoinj.wallet.SendRequest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.qora.account.PrivateKeyAccount;
import org.qora.account.PublicKeyAccount;
import org.qora.asset.Asset;
import org.qora.controller.Controller;
import org.qora.crosschain.BTC;
import org.qora.crosschain.BTCACCT;
@ -99,13 +92,12 @@ public class Initiate1 {
byte[] yourQortPrivKey = Base58.decode(yourQortPrivKey58);
PrivateKeyAccount yourQortalAccount = new PrivateKeyAccount(repository, yourQortPrivKey);
byte[] yourQortPubKey = yourQortalAccount.getPublicKey();
System.out.println(String.format("Your Qortal address: %s", yourQortalAccount.getAddress()));
byte[] yourBitcoinPubKey = HashCode.fromString(yourBitcoinPubKeyHex).asBytes();
ECKey yourBitcoinKey = ECKey.fromPublicOnly(yourBitcoinPubKey);
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);
PublicKeyAccount theirQortalAccount = new PublicKeyAccount(repository, theirQortPubKey);
@ -114,7 +106,15 @@ public class Initiate1 {
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.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
@ -129,7 +129,7 @@ public class Initiate1 {
System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString());
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);
System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString());
@ -139,10 +139,10 @@ public class Initiate1 {
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
System.out.println("P2SH address: " + p2shAddress.toString());
Coin bitcoinAmount = Coin.parseCoin(rawBitcoinAmount);
Coin bitcoinAmount = Coin.parseCoin(rawBitcoinAmount).add(BTCACCT.DEFAULT_BTC_FEE);
// 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");
} catch (NumberFormatException e) {

174
src/test/java/org/qora/test/btcacct/Refund2.java

@ -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());
}
}
}

52
src/test/java/org/qora/test/btcacct/Respond2.java

@ -1,13 +1,13 @@
package org.qora.test.btcacct;
import java.math.BigDecimal;
import java.security.SecureRandom;
import java.security.Security;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
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;
@ -20,7 +20,6 @@ import org.qora.asset.Asset;
import org.qora.controller.Controller;
import org.qora.crosschain.BTC;
import org.qora.crosschain.BTCACCT;
import org.qora.crypto.Crypto;
import org.qora.data.transaction.BaseTransactionData;
import org.qora.data.transaction.DeployAtTransactionData;
import org.qora.data.transaction.TransactionData;
@ -30,8 +29,11 @@ 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 org.qora.transaction.DeployAtTransaction;
import org.qora.transaction.Transaction;
import org.qora.transform.TransformationException;
import org.qora.transform.transaction.TransactionTransformer;
import org.qora.utils.Base58;
import com.google.common.hash.HashCode;
@ -60,12 +62,12 @@ public class Respond2 {
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("example: Respond2 pYQ6DpQBJ2n72TCLJLScEvwhf3boxWy2kQEPynakwpj \\\n"
+ "\t03aa20871c2195361f2826c7a649eab6b42639630c4d8c33c55311d5c1e476b5d6 \\\n"
+ "\t123 0.00008642 \\\n"
+ "\tJBNBQQDzZsm5do1BrwWAp53Ps4KYJVt749EGpCf7ofte \\\n"
System.err.println(String.format("example: Respond2 3jjoToDaDpsdUHqaouLGypFeewNVKvtkmdM38i54WVra \\\n"
+ "\t032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n"
+ "\te43f5ab106b70df2e85656de30e1862891752f81e82f5dfd03abb8465a7346f9 1574441679 2N4R2pSEzLcJgtgAbFuLvviwwEkBrmq6sx4"));
+ "\t123 0.00008642 \\\n"
+ "\t6rNn9b3pYRrG9UKqzMWYZ9qa8F3Zgv2mVWrULGHUusb \\\n"
+ "\t03aa20871c2195361f2826c7a649eab6b42639630c4d8c33c55311d5c1e476b5d6 \\\n"
+ "\tb837056cdc5d805e4db1f830a58158e1131ac96ea71de4c6f9d7854985e153e2 1575021641 2MvGdGUgAfc7qTHaZJwWmZ26Fg6Hjif8gNy"));
System.exit(1);
}
@ -74,6 +76,9 @@ public class Respond2 {
usage();
Security.insertProviderAt(new BouncyCastleProvider(), 0);
Settings.fileInstance("settings-test.json");
NetworkParameters params = TestNet3Params.get();
int argIndex = 0;
@ -108,7 +113,7 @@ public class Respond2 {
byte[] yourBitcoinPubKey = HashCode.fromString(yourBitcoinPubKeyHex).asBytes();
ECKey yourBitcoinKey = ECKey.fromPublicOnly(yourBitcoinPubKey);
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);
PublicKeyAccount theirQortalAccount = new PublicKeyAccount(repository, theirQortPubKey);
@ -117,7 +122,7 @@ public class Respond2 {
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.toString()));
System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress));
System.out.println("Hash of secret: " + secretHashHex);
@ -126,7 +131,7 @@ public class Respond2 {
System.out.println("\nCHECKING info from other party:");
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();
System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString());
@ -137,15 +142,26 @@ public class Respond2 {
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
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)) {
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);
}
// 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:");
@ -173,6 +189,16 @@ public class Respond2 {
deployAtTransactionData.setFee(fee);
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) {
usage();
} catch (DataException e) {

Loading…
Cancel
Save