From 5c0134c16a1ac80cde8a5208f3b262abf7631129 Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 6 Dec 2019 11:13:02 +0000 Subject: [PATCH] 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 --- .../java/org/qora/crosschain/BTCACCT.java | 55 ++- .../java/org/qortal/at/BlockchainAPI.java | 134 -------- src/main/java/org/qortal/at/QortalATAPI.java | 31 +- .../java/org/qortal/block/BlockChain.java | 21 ++ src/main/java/org/qortal/crosschain/BTC.java | 320 ++++++++++-------- src/main/resources/blockchain.json | 6 + .../java/org/qora/test/btcacct/Initiate1.java | 28 +- .../java/org/qora/test/btcacct/Refund2.java | 174 ++++++++++ .../java/org/qora/test/btcacct/Respond2.java | 52 ++- 9 files changed, 495 insertions(+), 326 deletions(-) delete mode 100644 src/main/java/org/qortal/at/BlockchainAPI.java create mode 100644 src/test/java/org/qora/test/btcacct/Refund2.java diff --git a/src/main/java/org/qora/crosschain/BTCACCT.java b/src/main/java/org/qora/crosschain/BTCACCT.java index 8cb6f062..f6670d44 100644 --- a/src/main/java/org/qora/crosschain/BTCACCT.java +++ b/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; diff --git a/src/main/java/org/qortal/at/BlockchainAPI.java b/src/main/java/org/qortal/at/BlockchainAPI.java deleted file mode 100644 index 2e91a0f6..00000000 --- a/src/main/java/org/qortal/at/BlockchainAPI.java +++ /dev/null @@ -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 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 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); - -} diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java index 8995c506..9be0e4c5 100644 --- a/src/main/java/org/qortal/at/QortalATAPI.java +++ b/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 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 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(); } diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index 305d9e4a..5d4c50fc 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/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); diff --git a/src/main/java/org/qortal/crosschain/BTC.java b/src/main/java/org/qortal/crosschain/BTC.java index 83a8bb07..d55b84f1 100644 --- a/src/main/java/org/qortal/crosschain/BTC.java +++ b/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 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 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 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; } } diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index 696701fd..1869438c 100644 --- a/src/main/resources/blockchain.json +++ b/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, diff --git a/src/test/java/org/qora/test/btcacct/Initiate1.java b/src/test/java/org/qora/test/btcacct/Initiate1.java index d04f2a41..60001176 100644 --- a/src/test/java/org/qora/test/btcacct/Initiate1.java +++ b/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) { diff --git a/src/test/java/org/qora/test/btcacct/Refund2.java b/src/test/java/org/qora/test/btcacct/Refund2.java new file mode 100644 index 00000000..05801775 --- /dev/null +++ b/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 ")); + 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 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()); + } + } + +} diff --git a/src/test/java/org/qora/test/btcacct/Respond2.java b/src/test/java/org/qora/test/btcacct/Respond2.java index 89937fb4..9dac26ed 100644 --- a/src/test/java/org/qora/test/btcacct/Respond2.java +++ b/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 ")); - 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) {