mirror of
https://github.com/Qortal/qortal.git
synced 2025-02-11 17:55:50 +00:00
CIYAM AT & cross-chain trading.
Bump CIYAM AT requirement to v1.3 Remove multi-blockchain AT aspect for now (BlockchainAPI). For PUT_PREVIOUS_BLOCK_HASH_INTO_A we no longer use SHA256 to condense 64-byte block signature into 32 bytes. Now we put block height into A1 and SHA192 of signature into A2 through A4. This allows possible future lookup of block data using "block hash", with verification that it is the same block. Some AT functions use "address in B" but sometimes we populate B with account's public key instead. So the method "getAccountFromB" is smart and checks for an actual, textual address in B starting with 'Q', otherwise assumes B contains public key. The Settings field "useBitcoinTestNet" (boolean) now replaced with "bitcoinNet" (String) with possible values MAIN (default), TEST3, REGTEST. This allows for more varied development/testing scenarios. Use correct Bitcoin nSequence value 0xFFFFFFFE for P2SH, i.e. enable locktime, disable RBF. Roll REGTEST checkpoints file generator into main BTC class. Yet another rewrite of Bitcoin P2SH scripts for BTC-QORT cross-chain trading. Added associated test classes BuildP2SH, CheckP2SH, DeployAT (unfinished).
This commit is contained in:
parent
8844cc0076
commit
87bb9090f5
Binary file not shown.
BIN
lib/org/ciyam/at/1.3/at-1.3.jar
Normal file
BIN
lib/org/ciyam/at/1.3/at-1.3.jar
Normal file
Binary file not shown.
9
lib/org/ciyam/at/1.3/at-1.3.pom
Normal file
9
lib/org/ciyam/at/1.3/at-1.3.pom
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>at</artifactId>
|
||||
<version>1.3</version>
|
||||
<description>POM was created from install:install-file</description>
|
||||
</project>
|
@ -3,11 +3,12 @@
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>at</artifactId>
|
||||
<versioning>
|
||||
<release>1.2</release>
|
||||
<release>1.3</release>
|
||||
<versions>
|
||||
<version>1.0</version>
|
||||
<version>1.2</version>
|
||||
<version>1.3</version>
|
||||
</versions>
|
||||
<lastUpdated>20191121173210</lastUpdated>
|
||||
<lastUpdated>20200408081355</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
2
pom.xml
2
pom.xml
@ -406,7 +406,7 @@
|
||||
<dependency>
|
||||
<groupId>org.ciyam</groupId>
|
||||
<artifactId>at</artifactId>
|
||||
<version>1.2</version>
|
||||
<version>1.3</version>
|
||||
</dependency>
|
||||
<!-- Bitcoin support -->
|
||||
<dependency>
|
||||
|
@ -1,153 +0,0 @@
|
||||
package org.qora.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.qora.account.Account;
|
||||
import org.qora.block.Block;
|
||||
import org.qora.data.block.BlockData;
|
||||
import org.qora.data.transaction.ATTransactionData;
|
||||
import org.qora.data.transaction.PaymentTransactionData;
|
||||
import org.qora.data.transaction.TransactionData;
|
||||
import org.qora.repository.BlockRepository;
|
||||
import org.qora.repository.DataException;
|
||||
import org.qora.transaction.Transaction;
|
||||
|
||||
public enum BlockchainAPI {
|
||||
|
||||
QORA(0) {
|
||||
@Override
|
||||
public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) {
|
||||
int height = timestamp.blockHeight;
|
||||
int sequence = timestamp.transactionSequence + 1;
|
||||
|
||||
QoraATAPI api = (QoraATAPI) state.getAPI();
|
||||
BlockRepository blockRepository = api.getRepository().getBlockRepository();
|
||||
|
||||
try {
|
||||
Account recipientAccount = new Account(api.getRepository(), 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.getRepository(), 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 = QoraATAPI.sha192(transaction.getTransactionData().getSignature());
|
||||
|
||||
api.setA2(state, QoraATAPI.fromBytes(hash, 0));
|
||||
api.setA3(state, QoraATAPI.fromBytes(hash, 8));
|
||||
api.setA4(state, QoraATAPI.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) {
|
||||
QoraATAPI api = (QoraATAPI) 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;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionOutput getIndexedOutputFromTransactionInA(MachineState state, int outputIndex) {
|
||||
// TODO
|
||||
return null;
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionOutput getIndexedOutputFromTransactionInA(MachineState state, int outputIndex) {
|
||||
// TODO
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
public static class TransactionOutput {
|
||||
byte[] recipient;
|
||||
long amount;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
public abstract TransactionOutput getIndexedOutputFromTransactionInA(MachineState state, int outputIndex);
|
||||
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
package org.qora.crosschain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.ECKey;
|
||||
@ -11,6 +12,7 @@ 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.Script;
|
||||
import org.bitcoinj.script.ScriptBuilder;
|
||||
import org.bitcoinj.script.ScriptChunk;
|
||||
import org.bitcoinj.script.ScriptOpCodes;
|
||||
@ -26,124 +28,149 @@ public class BTCACCT {
|
||||
|
||||
public static final Coin DEFAULT_BTC_FEE = Coin.valueOf(1000L); // 0.00001000 BTC
|
||||
|
||||
private static final byte[] redeemScript1 = HashCode.fromString("76a914").asBytes(); // OP_DUP OP_HASH160 push(0x14 bytes)
|
||||
private static final byte[] redeemScript2 = HashCode.fromString("88ada97614").asBytes(); // OP_EQUALVERIFY OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes)
|
||||
private static final byte[] redeemScript3 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes)
|
||||
private static final byte[] redeemScript4 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes)
|
||||
/*
|
||||
* OP_TUCK (to copy public key to before signature)
|
||||
* OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails)
|
||||
* OP_HASH160 (convert public key to PKH)
|
||||
* OP_DUP (duplicate PKH)
|
||||
* <push 20 bytes> <refund PKH> OP_EQUAL (does PKH match refund PKH?)
|
||||
* OP_IF
|
||||
* OP_DROP (no need for duplicate PKH)
|
||||
* <push 4 bytes> <locktime>
|
||||
* OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is <locktime> so script passes)
|
||||
* OP_ELSE
|
||||
* <push 20 bytes> <redeem PKH> OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails)
|
||||
* OP_HASH160 (hash secret)
|
||||
* <push 20 bytes> <hash of secret> OP_EQUAL (do hashes of secrets match? if true, script passes else script fails)
|
||||
* OP_ENDIF
|
||||
*/
|
||||
|
||||
private static final byte[] redeemScript1 = HashCode.fromString("7dada97614").asBytes(); // OP_TUCK OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes)
|
||||
private static final byte[] redeemScript2 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes)
|
||||
private static final byte[] redeemScript3 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes)
|
||||
private static final byte[] redeemScript4 = HashCode.fromString("88a914").asBytes(); // OP_EQUALVERIFY OP_HASH160 push(0x14 bytes)
|
||||
private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF
|
||||
|
||||
/**
|
||||
* Returns Bitcoin redeem script.
|
||||
* <p>
|
||||
* <pre>
|
||||
* OP_DUP OP_HASH160 push(0x14) <trade pubkeyhash> OP_EQUALVERIFY OP_CHECKSIGVERIFY
|
||||
* OP_HASH160 OP_DUP push(0x14) <sender/refund P2PKH> OP_EQUAL
|
||||
* OP_TUCK OP_CHECKSIGVERIFY
|
||||
* OP_HASH160 OP_DUP push(0x14) <refunder pubkeyhash> OP_EQUAL
|
||||
* OP_IF
|
||||
* OP_DROP push(0x04 bytes) <refund locktime> OP_CHECKLOCKTIMEVERIFY
|
||||
* OP_ELSE
|
||||
* push(0x14) <redeemer P2PKH> OP_EQUAL
|
||||
* push(0x14) <redeemer pubkeyhash> OP_EQUALVERIFY
|
||||
* OP_HASH160 push(0x14 bytes) <hash of secret> OP_EQUAL
|
||||
* OP_ENDIF
|
||||
* </pre>
|
||||
*
|
||||
* @param tradePubKeyHash
|
||||
* @param refunderPubKeyHash
|
||||
* @param senderPubKey
|
||||
* @param recipientPubKey
|
||||
* @param lockTime
|
||||
* @return
|
||||
*/
|
||||
public static byte[] buildScript(byte[] tradePubKeyHash, byte[] senderPubKeyHash, byte[] recipientPubKeyHash, int lockTime) {
|
||||
return Bytes.concat(redeemScript1, tradePubKeyHash, redeemScript2, senderPubKeyHash, redeemScript3, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
|
||||
redeemScript4, recipientPubKeyHash, redeemScript5);
|
||||
public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) {
|
||||
return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
|
||||
redeemScript3, redeemerPubKeyHash, redeemScript4, secretHash, redeemScript5);
|
||||
}
|
||||
|
||||
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey tradeKey, byte[] senderPubKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) {
|
||||
/**
|
||||
* Builds a custom transaction to spend P2SH.
|
||||
*
|
||||
* @param amount
|
||||
* @param spendKey
|
||||
* @param recipientPubKeyHash
|
||||
* @param fundingOutput
|
||||
* @param redeemScriptBytes
|
||||
* @param lockTime
|
||||
* @param scriptSigBuilder
|
||||
* @return
|
||||
*/
|
||||
public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, Long lockTime, Function<byte[], Script> scriptSigBuilder) {
|
||||
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
||||
|
||||
Transaction refundTransaction = new Transaction(params);
|
||||
refundTransaction.setVersion(2);
|
||||
Transaction transaction = new Transaction(params);
|
||||
transaction.setVersion(2);
|
||||
|
||||
// Output is back to P2SH funder
|
||||
ECKey senderKey = ECKey.fromPublicOnly(senderPubKey);
|
||||
refundTransaction.addOutput(refundAmount, ScriptBuilder.createP2PKHOutputScript(senderKey));
|
||||
transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(spendKey.getPubKeyHash()));
|
||||
|
||||
// Input (without scriptSig prior to signing)
|
||||
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
|
||||
input.setSequenceNumber(0); // Use 0, not max-value, so lockTime can be used
|
||||
refundTransaction.addInput(input);
|
||||
if (lockTime != null)
|
||||
input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF
|
||||
else
|
||||
input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF
|
||||
transaction.addInput(input);
|
||||
|
||||
// Set locktime after inputs added but before input signatures are generated
|
||||
refundTransaction.setLockTime(lockTime);
|
||||
if (lockTime != null)
|
||||
transaction.setLockTime(lockTime);
|
||||
|
||||
// Generate transaction signature for input
|
||||
final boolean anyoneCanPay = false;
|
||||
TransactionSignature txSig = refundTransaction.calculateSignature(0, tradeKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
|
||||
TransactionSignature txSig = transaction.calculateSignature(0, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
|
||||
|
||||
// Build scriptSig with...
|
||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||
|
||||
// sender/refund pubkey
|
||||
scriptBuilder.addChunk(new ScriptChunk(senderPubKey.length, senderPubKey));
|
||||
|
||||
// transaction signature
|
||||
// Calculate transaction signature
|
||||
byte[] txSigBytes = txSig.encodeToBitcoin();
|
||||
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
|
||||
|
||||
// trade public key
|
||||
byte[] tradePubKey = tradeKey.getPubKey();
|
||||
scriptBuilder.addChunk(new ScriptChunk(tradePubKey.length, tradePubKey));
|
||||
|
||||
/// redeem script
|
||||
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
||||
// Build scriptSig using lambda and tx signature
|
||||
Script scriptSig = scriptSigBuilder.apply(txSigBytes);
|
||||
|
||||
// Set input scriptSig
|
||||
refundTransaction.getInput(0).setScriptSig(scriptBuilder.build());
|
||||
transaction.getInput(0).setScriptSig(scriptSig);
|
||||
|
||||
return refundTransaction;
|
||||
return transaction;
|
||||
}
|
||||
|
||||
public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey tradeKey, byte[] recipientPubKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes) {
|
||||
NetworkParameters params = BTC.getInstance().getNetworkParameters();
|
||||
public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) {
|
||||
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
|
||||
// Build scriptSig with...
|
||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||
|
||||
Transaction redeemTransaction = new Transaction(params);
|
||||
redeemTransaction.setVersion(2);
|
||||
// transaction signature
|
||||
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
|
||||
|
||||
// Output to redeem recipient
|
||||
ECKey senderKey = ECKey.fromPublicOnly(recipientPubKey);
|
||||
redeemTransaction.addOutput(redeemAmount, ScriptBuilder.createP2PKHOutputScript(senderKey));
|
||||
// redeem public key
|
||||
byte[] refundPubKey = refundKey.getPubKey();
|
||||
scriptBuilder.addChunk(new ScriptChunk(refundPubKey.length, refundPubKey));
|
||||
|
||||
// Input (without scriptSig prior to signing)
|
||||
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
|
||||
input.setSequenceNumber(0); // Use 0, not max-value, so lockTime can be used
|
||||
redeemTransaction.addInput(input);
|
||||
// redeem script
|
||||
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
||||
|
||||
// Generate transaction signature for input
|
||||
final boolean anyoneCanPay = false;
|
||||
TransactionSignature txSig = redeemTransaction.calculateSignature(0, tradeKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
|
||||
return scriptBuilder.build();
|
||||
};
|
||||
|
||||
// Build scriptSig with...
|
||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||
|
||||
// recipient pubkey
|
||||
scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey));
|
||||
|
||||
// transaction signature
|
||||
byte[] txSigBytes = txSig.encodeToBitcoin();
|
||||
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
|
||||
|
||||
// trade public key
|
||||
byte[] tradePubKey = tradeKey.getPubKey();
|
||||
scriptBuilder.addChunk(new ScriptChunk(tradePubKey.length, tradePubKey));
|
||||
|
||||
/// redeem script
|
||||
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
||||
|
||||
// Set input scriptSig
|
||||
redeemTransaction.getInput(0).setScriptSig(scriptBuilder.build());
|
||||
|
||||
return redeemTransaction;
|
||||
return buildP2shTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, lockTime, refundSigScriptBuilder);
|
||||
}
|
||||
|
||||
public static byte[] buildCiyamAT(byte[] secretHash, byte[] destinationQortalPubKey, long refundMinutes) {
|
||||
public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, byte[] secret) {
|
||||
Function<byte[], Script> redeemSigScriptBuilder = (txSigBytes) -> {
|
||||
// Build scriptSig with...
|
||||
ScriptBuilder scriptBuilder = new ScriptBuilder();
|
||||
|
||||
// secret
|
||||
scriptBuilder.addChunk(new ScriptChunk(secret.length, secret));
|
||||
|
||||
// transaction signature
|
||||
scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
|
||||
|
||||
// redeem public key
|
||||
byte[] redeemPubKey = redeemKey.getPubKey();
|
||||
scriptBuilder.addChunk(new ScriptChunk(redeemPubKey.length, redeemPubKey));
|
||||
|
||||
// redeem script
|
||||
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
|
||||
|
||||
return scriptBuilder.build();
|
||||
};
|
||||
|
||||
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, null, redeemSigScriptBuilder);
|
||||
}
|
||||
|
||||
public static byte[] buildQortalAT(byte[] secretHash, String destinationQortalAddress, long refundMinutes, BigDecimal initialPayout) {
|
||||
// Labels for data segment addresses
|
||||
int addrCounter = 0;
|
||||
final int addrHashPart1 = addrCounter++;
|
||||
@ -155,6 +182,9 @@ public class BTCACCT {
|
||||
final int addrAddressPart3 = addrCounter++;
|
||||
final int addrAddressPart4 = addrCounter++;
|
||||
final int addrRefundMinutes = addrCounter++;
|
||||
final int addrHashTempIndex = addrCounter++;
|
||||
final int addrHashTempLength = addrCounter++;
|
||||
final int addrInitialPayoutAmount = addrCounter++;
|
||||
final int addrRefundTimestamp = addrCounter++;
|
||||
final int addrLastTimestamp = addrCounter++;
|
||||
final int addrBlockTimestamp = addrCounter++;
|
||||
@ -164,19 +194,30 @@ public class BTCACCT {
|
||||
final int addrAddressTemp2 = addrCounter++;
|
||||
final int addrAddressTemp3 = addrCounter++;
|
||||
final int addrAddressTemp4 = addrCounter++;
|
||||
final int addrHashTemp1 = addrCounter++;
|
||||
final int addrHashTemp2 = addrCounter++;
|
||||
final int addrHashTemp3 = addrCounter++;
|
||||
final int addrHashTemp4 = addrCounter++;
|
||||
|
||||
// Data segment
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * 8).order(ByteOrder.LITTLE_ENDIAN);
|
||||
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * 8);
|
||||
|
||||
// Hash of secret into HashPart1-4
|
||||
dataByteBuffer.put(secretHash);
|
||||
|
||||
// Destination Qortal account's public key
|
||||
dataByteBuffer.put(destinationQortalPubKey);
|
||||
dataByteBuffer.put(Bytes.ensureCapacity(destinationQortalAddress.getBytes(), 32, 0));
|
||||
|
||||
// Expiry in minutes
|
||||
dataByteBuffer.putLong(refundMinutes);
|
||||
|
||||
// Temp buffer for hashing any passed secret
|
||||
dataByteBuffer.putLong(addrHashTemp1);
|
||||
dataByteBuffer.putLong(32L);
|
||||
|
||||
// Initial payout amount
|
||||
dataByteBuffer.putLong(initialPayout.unscaledValue().longValue());
|
||||
|
||||
// Code labels
|
||||
final int addrTxLoop = 0x36;
|
||||
final int addrCheckTx = 0x4b;
|
||||
@ -187,13 +228,17 @@ public class BTCACCT {
|
||||
final int addrEndOfCode = 0x109;
|
||||
|
||||
int tempPC;
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(addrEndOfCode * 1).order(ByteOrder.LITTLE_ENDIAN);
|
||||
ByteBuffer codeByteBuffer = ByteBuffer.allocate(addrEndOfCode * 1);
|
||||
|
||||
// init:
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_CREATION_TIMESTAMP.value).putInt(addrRefundTimestamp);
|
||||
codeByteBuffer.put(OpCode.SET_DAT.value).putInt(addrLastTimestamp).putInt(addrRefundTimestamp);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(FunctionCode.ADD_MINUTES_TO_TIMESTAMP.value).putInt(addrRefundTimestamp)
|
||||
.putInt(addrRefundTimestamp).putInt(addrRefundMinutes);
|
||||
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrAddressPart1);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PAY_TO_ADDRESS_IN_B.value).putInt(addrInitialPayoutAmount);
|
||||
|
||||
codeByteBuffer.put(OpCode.SET_PCS.value);
|
||||
|
||||
// loop:
|
||||
@ -204,7 +249,7 @@ public class BTCACCT {
|
||||
|
||||
// txloop:
|
||||
assert codeByteBuffer.position() == addrTxLoop : "addrTxLoop incorrect";
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_IN_A.value).putInt(addrLastTimestamp);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(addrLastTimestamp);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_IS_ZERO.value).putInt(addrComparator);
|
||||
tempPC = codeByteBuffer.position();
|
||||
codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(addrComparator).put((byte) (addrCheckTx - tempPC));
|
||||
@ -220,10 +265,7 @@ public class BTCACCT {
|
||||
// checkSender
|
||||
assert codeByteBuffer.position() == addrCheckSender : "addrCheckSender incorrect";
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B.value);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B1.value).putInt(addrAddressTemp1);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B2.value).putInt(addrAddressTemp2);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B3.value).putInt(addrAddressTemp3);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B4.value).putInt(addrAddressTemp4);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B_IND.value).putInt(addrAddressTemp1);
|
||||
tempPC = codeByteBuffer.position();
|
||||
codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp1).putInt(addrAddressPart1).put((byte) (addrTxLoop - tempPC));
|
||||
tempPC = codeByteBuffer.position();
|
||||
@ -236,23 +278,16 @@ public class BTCACCT {
|
||||
// checkMessage:
|
||||
assert codeByteBuffer.position() == addrCheckMessage : "addrCheckMessage incorrect";
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B.value);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.SWAP_A_AND_B.value);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(addrHashPart1);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(addrHashPart2);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(addrHashPart3);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B4.value).putInt(addrHashPart4);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_SHA256_A_WITH_B.value).putInt(addrComparator);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B_IND.value).putInt(addrHashTemp1);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrHashPart1);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_HASH160_WITH_B.value).putInt(addrHashTempIndex).putInt(addrHashTempLength);
|
||||
tempPC = codeByteBuffer.position();
|
||||
codeByteBuffer.put(OpCode.BNZ_DAT.value).putInt(addrComparator).put((byte) (addrPayout - tempPC));
|
||||
codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(addrTxLoop);
|
||||
|
||||
// payout:
|
||||
assert codeByteBuffer.position() == addrPayout : "addrPayout incorrect";
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(addrAddressPart1);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(addrAddressPart2);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(addrAddressPart3);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B4.value).putInt(addrAddressPart4);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.MESSAGE_A_TO_ADDRESS_IN_B.value);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrAddressPart1);
|
||||
codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B.value);
|
||||
codeByteBuffer.put(OpCode.FIN_IMD.value);
|
||||
|
||||
|
@ -17,19 +17,24 @@ import org.qortal.account.Account;
|
||||
import org.qortal.account.GenesisAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.block.Block;
|
||||
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;
|
||||
import org.qortal.data.block.BlockSummaryData;
|
||||
import org.qortal.data.transaction.ATTransactionData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.PaymentTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.BlockRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.AtTransaction;
|
||||
import org.qortal.transaction.Transaction;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
@ -108,31 +113,90 @@ public class QortalATAPI extends API {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putPreviousBlockHashInA(MachineState state) {
|
||||
public void putPreviousBlockHashIntoA(MachineState state) {
|
||||
try {
|
||||
BlockData blockData = this.repository.getBlockRepository().fromHeight(this.getPreviousBlockHeight());
|
||||
int previousBlockHeight = this.repository.getBlockRepository().getBlockchainHeight() - 1;
|
||||
|
||||
// We only need signature, so only request a block summary
|
||||
List<BlockSummaryData> blockSummaries = this.repository.getBlockRepository().getBlockSummaries(previousBlockHeight, previousBlockHeight);
|
||||
if (blockSummaries == null || blockSummaries.size() != 1)
|
||||
throw new RuntimeException("AT API unable to fetch previous block hash?");
|
||||
|
||||
// Block's signature is 128 bytes so we need to reduce this to 4 longs (32 bytes)
|
||||
byte[] blockHash = Crypto.digest(blockData.getSignature());
|
||||
// To be able to use hash to look up block, save height (8 bytes) and rehash with SHA192 (24 bytes)
|
||||
this.setA1(state, previousBlockHeight);
|
||||
|
||||
this.setA(state, blockHash);
|
||||
byte[] sigHash192 = sha192(blockSummaries.get(0).getSignature());
|
||||
this.setA2(state, fromBytes(sigHash192, 0));
|
||||
this.setA3(state, fromBytes(sigHash192, 8));
|
||||
this.setA4(state, fromBytes(sigHash192, 16));
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch previous block?", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) {
|
||||
public void putTransactionAfterTimestampIntoA(Timestamp timestamp, MachineState state) {
|
||||
// Recipient is this AT
|
||||
String recipient = this.atData.getATAddress();
|
||||
|
||||
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
|
||||
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
|
||||
int height = timestamp.blockHeight;
|
||||
int sequence = timestamp.transactionSequence + 1;
|
||||
|
||||
BlockRepository blockRepository = this.getRepository().getBlockRepository();
|
||||
|
||||
try {
|
||||
Account recipientAccount = new Account(this.getRepository(), recipient);
|
||||
int currentHeight = blockRepository.getBlockchainHeight();
|
||||
|
||||
while (height <= currentHeight) {
|
||||
BlockData blockData = blockRepository.fromHeight(height);
|
||||
|
||||
if (blockData == null)
|
||||
throw new DataException("Unable to fetch block " + height + " from repository?");
|
||||
|
||||
Block block = new Block(this.getRepository(), blockData);
|
||||
|
||||
List<Transaction> blockTransactions = block.getTransactions();
|
||||
|
||||
// No more transactions in this block? Try next block
|
||||
if (sequence >= blockTransactions.size()) {
|
||||
++height;
|
||||
sequence = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
Transaction transaction = blockTransactions.get(sequence);
|
||||
|
||||
// Transaction needs to be sent to specified recipient
|
||||
if (transaction.getRecipientAccounts().contains(recipientAccount)) {
|
||||
// Found a transaction
|
||||
|
||||
this.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[] sigHash192 = sha192(transaction.getTransactionData().getSignature());
|
||||
this.setA2(state, fromBytes(sigHash192, 0));
|
||||
this.setA3(state, fromBytes(sigHash192, 8));
|
||||
this.setA4(state, fromBytes(sigHash192, 16));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Transaction wasn't for us - keep going
|
||||
++sequence;
|
||||
}
|
||||
|
||||
// No more transactions - zero A and exit
|
||||
this.zeroA(state);
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("AT API unable to fetch next transaction?", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTypeFromTransactionInA(MachineState state) {
|
||||
TransactionData transactionData = this.fetchTransaction(state);
|
||||
TransactionData transactionData = this.getTransactionFromA(state);
|
||||
|
||||
switch (transactionData.getType()) {
|
||||
case PAYMENT:
|
||||
@ -154,9 +218,23 @@ public class QortalATAPI extends API {
|
||||
|
||||
@Override
|
||||
public long getAmountFromTransactionInA(MachineState state) {
|
||||
Timestamp timestamp = new Timestamp(state.getA1());
|
||||
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
|
||||
return blockchainAPI.getAmountFromTransactionInA(timestamp, state);
|
||||
TransactionData transactionData = this.getTransactionFromA(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();
|
||||
|
||||
// fall-through to default
|
||||
|
||||
default:
|
||||
return 0xffffffffffffffffL;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -168,8 +246,8 @@ public class QortalATAPI extends API {
|
||||
|
||||
@Override
|
||||
public long generateRandomUsingTransactionInA(MachineState state) {
|
||||
// The plan here is to sleep for a block then use next block's signature and this transaction's signature to generate pseudo-random, but deterministic,
|
||||
// value.
|
||||
// The plan here is to sleep for a block then use next block's signature
|
||||
// and this transaction's signature to generate pseudo-random, but deterministic, value.
|
||||
|
||||
if (!isFirstOpCodeAfterSleeping(state)) {
|
||||
// First call
|
||||
@ -182,7 +260,7 @@ public class QortalATAPI extends API {
|
||||
// Second call
|
||||
|
||||
// HASH(A and new block hash)
|
||||
TransactionData transactionData = this.fetchTransaction(state);
|
||||
TransactionData transactionData = this.getTransactionFromA(state);
|
||||
|
||||
try {
|
||||
BlockData blockData = this.repository.getBlockRepository().getLastBlock();
|
||||
@ -206,7 +284,7 @@ public class QortalATAPI extends API {
|
||||
// Zero B in case of issues or shorter-than-B message
|
||||
this.zeroB(state);
|
||||
|
||||
TransactionData transactionData = this.fetchTransaction(state);
|
||||
TransactionData transactionData = this.getTransactionFromA(state);
|
||||
|
||||
byte[] messageData = null;
|
||||
|
||||
@ -236,7 +314,7 @@ public class QortalATAPI extends API {
|
||||
|
||||
@Override
|
||||
public void putAddressFromTransactionInAIntoB(MachineState state) {
|
||||
TransactionData transactionData = this.fetchTransaction(state);
|
||||
TransactionData transactionData = this.getTransactionFromA(state);
|
||||
|
||||
// We actually use public key as it has more potential utility (e.g. message verification) than an address
|
||||
byte[] bytes = transactionData.getCreatorPublicKey();
|
||||
@ -265,9 +343,7 @@ public class QortalATAPI extends API {
|
||||
|
||||
@Override
|
||||
public void payAmountToB(long unscaledAmount, MachineState state) {
|
||||
byte[] publicKey = state.getB();
|
||||
|
||||
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
|
||||
Account recipient = getAccountFromB(state);
|
||||
|
||||
long timestamp = this.getNextTransactionTimestamp();
|
||||
byte[] reference = this.getLastReference();
|
||||
@ -285,9 +361,7 @@ public class QortalATAPI extends API {
|
||||
@Override
|
||||
public void messageAToB(MachineState state) {
|
||||
byte[] message = state.getA();
|
||||
byte[] publicKey = state.getB();
|
||||
|
||||
PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey);
|
||||
Account recipient = getAccountFromB(state);
|
||||
|
||||
long timestamp = this.getNextTransactionTimestamp();
|
||||
byte[] reference = this.getLastReference();
|
||||
@ -306,7 +380,7 @@ public class QortalATAPI extends API {
|
||||
int blockHeight = timestamp.blockHeight;
|
||||
|
||||
// At least one block in the future
|
||||
blockHeight += (minutes / this.ciyamAtSettings.minutesPerBlock) + 1;
|
||||
blockHeight += Math.max(minutes / this.ciyamAtSettings.minutesPerBlock, 1);
|
||||
|
||||
return new Timestamp(blockHeight, 0).longValue();
|
||||
}
|
||||
@ -380,7 +454,7 @@ public class QortalATAPI extends API {
|
||||
}
|
||||
|
||||
/** Returns transaction data from repository using block height & sequence from A1, checking the transaction signatures match too */
|
||||
/* package */ TransactionData fetchTransaction(MachineState state) {
|
||||
/* package */ TransactionData getTransactionFromA(MachineState state) {
|
||||
Timestamp timestamp = new Timestamp(state.getA1());
|
||||
|
||||
try {
|
||||
@ -415,11 +489,6 @@ public class QortalATAPI extends API {
|
||||
* Timestamp is block's timestamp + position in AT-Transactions list.
|
||||
*
|
||||
* We need increasing timestamps to preserve transaction order and hence a correct signature-reference chain when the block is processed.
|
||||
*
|
||||
* As Qora blocks must share the same milliseconds component in their timestamps, this allows us to generate up to 1,000 AT-Transactions per AT without
|
||||
* issue.
|
||||
*
|
||||
* As long as ATs are not allowed to generate that many per block, e.g. by limiting maximum steps per execution round, then we should be fine.
|
||||
*/
|
||||
|
||||
// XXX THE ABOVE IS NO LONGER TRUE IN QORTAL!
|
||||
@ -443,4 +512,27 @@ public class QortalATAPI extends API {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Account (possibly PublicKeyAccount) based on value in B.
|
||||
* <p>
|
||||
* If bytes in B start with 'Q' then use B as an address, but only if valid.
|
||||
* <p>
|
||||
* Otherwise, assume B is a public key.
|
||||
* @return
|
||||
*/
|
||||
private Account getAccountFromB(MachineState state) {
|
||||
byte[] bBytes = state.getB();
|
||||
|
||||
if (bBytes[0] == 'Q') {
|
||||
int zeroIndex = Bytes.indexOf(bBytes, (byte) 0);
|
||||
if (zeroIndex > 0) {
|
||||
String address = new String(bBytes, 0, zeroIndex);
|
||||
if (Crypto.isValidAddress(address))
|
||||
return new Account(this.repository, address);
|
||||
}
|
||||
}
|
||||
|
||||
return new PublicKeyAccount(this.repository, bBytes);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package org.qortal.at;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
@ -10,6 +9,7 @@ import org.ciyam.at.FunctionData;
|
||||
import org.ciyam.at.IllegalFunctionCodeException;
|
||||
import org.ciyam.at.MachineState;
|
||||
import org.ciyam.at.Timestamp;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
@ -20,52 +20,6 @@ import org.qortal.settings.Settings;
|
||||
*
|
||||
*/
|
||||
public enum QortalFunctionCode {
|
||||
/**
|
||||
* <tt>0x0500</tt><br>
|
||||
* Returns current BTC block's "timestamp".
|
||||
*/
|
||||
GET_BTC_BLOCK_TIMESTAMP(0x0500, 0, true) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
functionData.returnValue = Timestamp.toLong(state.getAPI().getCurrentBlockHeight(), BlockchainAPI.BTC.value, 0);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* <tt>0x0501</tt><br>
|
||||
* Put transaction from specific recipient after timestamp in A, or zero if none.
|
||||
*/
|
||||
PUT_TX_FROM_B_RECIPIENT_AFTER_TIMESTAMP_IN_A(0x0501, 1, false) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
Timestamp timestamp = new Timestamp(functionData.value2);
|
||||
|
||||
String recipient = new String(state.getB(), StandardCharsets.UTF_8);
|
||||
|
||||
BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId);
|
||||
blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* <tt>0x0502</tt><br>
|
||||
* Get output, using transaction in A and passed index, putting address in B and returning amount.<br>
|
||||
* Return -1 if no such output;
|
||||
*/
|
||||
GET_INDEXED_OUTPUT(0x0502, 1, true) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
int outputIndex = (int) (functionData.value1 & 0xffffffffL);
|
||||
|
||||
BlockchainAPI.TransactionOutput output = BlockchainAPI.BTC.getIndexedOutputFromTransactionInA(state, outputIndex);
|
||||
|
||||
if (output == null) {
|
||||
functionData.returnValue = -1L;
|
||||
return;
|
||||
}
|
||||
|
||||
state.getAPI().setB(state, output.recipient);
|
||||
functionData.returnValue = output.amount;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* <tt>0x0510</tt><br>
|
||||
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.
|
||||
@ -90,7 +44,7 @@ public enum QortalFunctionCode {
|
||||
CONVERT_B_TO_P2SH(0x0511, 0, false) {
|
||||
@Override
|
||||
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
|
||||
byte addressPrefix = Settings.getInstance().useBitcoinTestNet() ? (byte) 0xc4 : 0x05;
|
||||
byte addressPrefix = Settings.getInstance().getBitcoinNet() == BTC.BitcoinNet.MAIN ? 0x05 : (byte) 0xc4;
|
||||
|
||||
convertAddressInB(addressPrefix, state);
|
||||
}
|
||||
|
@ -10,8 +10,11 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@ -19,8 +22,9 @@ import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@ -46,16 +50,19 @@ import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bitcoinj.store.BlockStore;
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
import org.bitcoinj.store.MemoryBlockStore;
|
||||
import org.bitcoinj.utils.MonetaryFormat;
|
||||
import org.bitcoinj.utils.Threading;
|
||||
import org.bitcoinj.wallet.Wallet;
|
||||
import org.bitcoinj.wallet.WalletTransaction;
|
||||
import org.bitcoinj.wallet.WalletTransaction.Pool;
|
||||
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
|
||||
import org.bitcoinj.wallet.listeners.WalletCoinsSentEventListener;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public class BTC {
|
||||
|
||||
public static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode();
|
||||
public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL;
|
||||
public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
|
||||
|
||||
private static final MessageDigest RIPE_MD160_DIGESTER;
|
||||
private static final MessageDigest SHA256_DIGESTER;
|
||||
static {
|
||||
@ -69,6 +76,29 @@ public class BTC {
|
||||
|
||||
protected static final Logger LOGGER = LogManager.getLogger(BTC.class);
|
||||
|
||||
public enum BitcoinNet {
|
||||
MAIN {
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return MainNetParams.get();
|
||||
}
|
||||
},
|
||||
TEST3 {
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return TestNet3Params.get();
|
||||
}
|
||||
},
|
||||
REGTEST {
|
||||
@Override
|
||||
public NetworkParameters getParams() {
|
||||
return RegTestParams.get();
|
||||
}
|
||||
};
|
||||
|
||||
public abstract NetworkParameters getParams();
|
||||
}
|
||||
|
||||
private static BTC instance;
|
||||
|
||||
private final NetworkParameters params;
|
||||
@ -85,24 +115,60 @@ public class BTC {
|
||||
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));
|
||||
public UpdateableCheckpointManager(NetworkParameters params, File checkpointsFile) throws IOException {
|
||||
super(params, getMinimalTextFileStream(params, checkpointsFile));
|
||||
}
|
||||
|
||||
public UpdateableCheckpointManager(NetworkParameters params, InputStream inputStream) throws IOException {
|
||||
super(params, inputStream);
|
||||
}
|
||||
|
||||
private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params) {
|
||||
private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params, File checkpointsFile) {
|
||||
if (params == MainNetParams.get())
|
||||
return new ByteArrayInputStream(MINIMAL_MAINNET_TEXTFILE.getBytes());
|
||||
|
||||
if (params == TestNet3Params.get())
|
||||
return new ByteArrayInputStream(MINIMAL_TESTNET3_TEXTFILE.getBytes());
|
||||
|
||||
if (params == RegTestParams.get())
|
||||
return newRegTestCheckpointsStream(checkpointsFile); // We have to build this
|
||||
|
||||
throw new RuntimeException("Failed to construct empty UpdateableCheckpointManageer");
|
||||
}
|
||||
|
||||
private static ByteArrayInputStream newRegTestCheckpointsStream(File checkpointsFile) {
|
||||
try {
|
||||
final NetworkParameters params = RegTestParams.get();
|
||||
|
||||
final BlockStore store = new MemoryBlockStore(params);
|
||||
final BlockChain chain = new BlockChain(params, store);
|
||||
final PeerGroup peerGroup = new PeerGroup(params, chain);
|
||||
|
||||
final InetAddress ipAddress = InetAddress.getLoopbackAddress();
|
||||
final PeerAddress peerAddress = new PeerAddress(params, ipAddress);
|
||||
peerGroup.addAddress(peerAddress);
|
||||
peerGroup.start();
|
||||
|
||||
final TreeMap<Integer, StoredBlock> checkpoints = new TreeMap<>();
|
||||
chain.addNewBestBlockListener((block) -> checkpoints.put(block.getHeight(), block));
|
||||
|
||||
peerGroup.downloadBlockChain();
|
||||
peerGroup.stop();
|
||||
|
||||
saveAsText(checkpointsFile, checkpoints.values());
|
||||
|
||||
return new ByteArrayInputStream(Files.readAllBytes(checkpointsFile.toPath()));
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (UnknownHostException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyNewBestBlock(StoredBlock block) {
|
||||
final int height = block.getHeight();
|
||||
@ -119,22 +185,22 @@ public class BTC {
|
||||
this.checkpoints.put(blockTimestamp, block);
|
||||
|
||||
try {
|
||||
this.saveAsText(new File(BTC.getInstance().getDirectory(), BTC.getInstance().getCheckpointsFileName()));
|
||||
saveAsText(new File(BTC.getInstance().getDirectory(), BTC.getInstance().getCheckpointsFileName()), this.checkpoints.values());
|
||||
} catch (FileNotFoundException e) {
|
||||
// Save failed - log it but it's not critical
|
||||
LOGGER.warn("Failed to save updated BTC checkpoints: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void saveAsText(File textFile) throws FileNotFoundException {
|
||||
private static void saveAsText(File textFile, Collection<StoredBlock> checkpointBlocks) throws FileNotFoundException {
|
||||
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(textFile), StandardCharsets.US_ASCII))) {
|
||||
writer.println("TXT CHECKPOINTS 1");
|
||||
writer.println("0"); // Number of signatures to read. Do this later.
|
||||
writer.println(this.checkpoints.size());
|
||||
writer.println(checkpointBlocks.size());
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE);
|
||||
|
||||
for (StoredBlock block : this.checkpoints.values()) {
|
||||
for (StoredBlock block : checkpointBlocks) {
|
||||
block.serializeCompact(buffer);
|
||||
writer.println(CheckpointManager.BASE64.encode(buffer.array()));
|
||||
buffer.position(0);
|
||||
@ -173,16 +239,24 @@ public class BTC {
|
||||
// Constructors and instance
|
||||
|
||||
private BTC() {
|
||||
if (Settings.getInstance().useBitcoinTestNet()) {
|
||||
/*
|
||||
this.params = RegTestParams.get();
|
||||
this.checkpointsFileName = "checkpoints-regtest.txt";
|
||||
*/
|
||||
this.params = TestNet3Params.get();
|
||||
this.checkpointsFileName = "checkpoints-testnet.txt";
|
||||
} else {
|
||||
this.params = MainNetParams.get();
|
||||
this.checkpointsFileName = "checkpoints.txt";
|
||||
BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet();
|
||||
this.params = bitcoinNet.getParams();
|
||||
|
||||
switch (bitcoinNet) {
|
||||
case MAIN:
|
||||
this.checkpointsFileName = "checkpoints.txt";
|
||||
break;
|
||||
|
||||
case TEST3:
|
||||
this.checkpointsFileName = "checkpoints-testnet.txt";
|
||||
break;
|
||||
|
||||
case REGTEST:
|
||||
this.checkpointsFileName = "checkpoints-regtest.txt";
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported Bitcoin network: " + bitcoinNet.name());
|
||||
}
|
||||
|
||||
this.directory = new File("Qortal-BTC");
|
||||
@ -196,7 +270,7 @@ public class BTC {
|
||||
} catch (FileNotFoundException e) {
|
||||
// Construct with no checkpoints then
|
||||
try {
|
||||
this.manager = new UpdateableCheckpointManager(this.params);
|
||||
this.manager = new UpdateableCheckpointManager(this.params, checkpointsFile);
|
||||
} catch (IOException e2) {
|
||||
throw new RuntimeException("Failed to create new BTC checkpoints", e2);
|
||||
}
|
||||
@ -222,7 +296,7 @@ public class BTC {
|
||||
return this.checkpointsFileName;
|
||||
}
|
||||
|
||||
/* package */ NetworkParameters getNetworkParameters() {
|
||||
public NetworkParameters getNetworkParameters() {
|
||||
return this.params;
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import org.eclipse.persistence.exceptions.XMLMarshalException;
|
||||
import org.eclipse.persistence.jaxb.JAXBContextFactory;
|
||||
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.crosschain.BTC.BitcoinNet;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@ -91,7 +92,7 @@ public class Settings {
|
||||
|
||||
// Which blockchains this node is running
|
||||
private String blockchainConfig = null; // use default from resources
|
||||
private boolean useBitcoinTestNet = false;
|
||||
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
|
||||
|
||||
// Repository related
|
||||
/** Queries that take longer than this are logged. (milliseconds) */
|
||||
@ -345,8 +346,8 @@ public class Settings {
|
||||
return this.blockchainConfig;
|
||||
}
|
||||
|
||||
public boolean useBitcoinTestNet() {
|
||||
return this.useBitcoinTestNet;
|
||||
public BitcoinNet getBitcoinNet() {
|
||||
return this.bitcoinNet;
|
||||
}
|
||||
|
||||
public Long getSlowQueryThreshold() {
|
||||
|
@ -16,8 +16,6 @@ import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.PeerAddress;
|
||||
import org.bitcoinj.core.PeerGroup;
|
||||
import org.bitcoinj.core.StoredBlock;
|
||||
import org.bitcoinj.core.VerificationException;
|
||||
import org.bitcoinj.core.listeners.NewBestBlockListener;
|
||||
import org.bitcoinj.params.RegTestParams;
|
||||
import org.bitcoinj.store.BlockStore;
|
||||
import org.bitcoinj.store.MemoryBlockStore;
|
||||
@ -33,7 +31,7 @@ public class BuildCheckpoints {
|
||||
final BlockChain chain = new BlockChain(params, store);
|
||||
final PeerGroup peerGroup = new PeerGroup(params, chain);
|
||||
|
||||
final InetAddress ipAddress = InetAddress.getLocalHost();
|
||||
final InetAddress ipAddress = InetAddress.getLoopbackAddress();
|
||||
final PeerAddress peerAddress = new PeerAddress(params, ipAddress);
|
||||
peerGroup.addAddress(peerAddress);
|
||||
peerGroup.start();
|
||||
|
125
src/test/java/org/qora/test/btcacct/BuildP2SH.java
Normal file
125
src/test/java/org/qora/test/btcacct/BuildP2SH.java
Normal file
@ -0,0 +1,125 @@
|
||||
package org.qora.test.btcacct;
|
||||
|
||||
import java.security.Security;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
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;
|
||||
|
||||
public class BuildP2SH {
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: BuildP2SH <refund-BTC-P2PKH> <BTC-amount> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)"));
|
||||
System.err.println(String.format("example: BuildP2SH "
|
||||
+ "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
|
||||
+ "\t0.00008642 \\\n"
|
||||
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
|
||||
+ "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 5 || args.length > 6)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
Address refundBitcoinAddress = null;
|
||||
Coin bitcoinAmount = null;
|
||||
Address redeemBitcoinAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
refundBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Refund BTC address must be in P2PKH form");
|
||||
|
||||
bitcoinAmount = Coin.parseCoin(args[argIndex++]);
|
||||
|
||||
redeemBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem BTC address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L);
|
||||
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 7 * 24 * 60 * 60)
|
||||
usage("Locktime (seconds) should be at between 10 minutes and 1 week from now");
|
||||
|
||||
if (args.length > argIndex)
|
||||
bitcoinFee = Coin.parseCoin(args[argIndex++]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
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:");
|
||||
|
||||
System.out.println(String.format("Refund Bitcoin address: %s", refundBitcoinAddress));
|
||||
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
|
||||
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
|
||||
|
||||
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
bitcoinAmount = bitcoinAmount.add(bitcoinFee);
|
||||
|
||||
// Fund P2SH
|
||||
System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)",
|
||||
p2shAddress.toString(), BTC.FORMAT.format(bitcoinAmount), BTC.FORMAT.format(bitcoinFee)));
|
||||
|
||||
System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT");
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("Repository issue: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
172
src/test/java/org/qora/test/btcacct/CheckP2SH.java
Normal file
172
src/test/java/org/qora/test/btcacct/CheckP2SH.java
Normal file
@ -0,0 +1,172 @@
|
||||
package org.qora.test.btcacct;
|
||||
|
||||
import java.security.Security;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
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;
|
||||
|
||||
public class CheckP2SH {
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: CheckP2SH <P2SH-address> <refund-BTC-P2PKH> <BTC-amount> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)"));
|
||||
System.err.println(String.format("example: CheckP2SH "
|
||||
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
|
||||
+ "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
|
||||
+ "\t0.00008642 \\\n"
|
||||
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
|
||||
+ "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 6 || args.length > 7)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
Address p2shAddress = null;
|
||||
Address refundBitcoinAddress = null;
|
||||
Coin bitcoinAmount = null;
|
||||
Address redeemBitcoinAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
p2shAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
|
||||
usage("P2SH address invalid");
|
||||
|
||||
refundBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Refund BTC address must be in P2PKH form");
|
||||
|
||||
bitcoinAmount = Coin.parseCoin(args[argIndex++]);
|
||||
|
||||
redeemBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Redeem BTC address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L);
|
||||
if (refundTimeoutDelay < 600 || refundTimeoutDelay > 7 * 24 * 60 * 60)
|
||||
usage("Locktime (seconds) should be at between 10 minutes and 1 week from now");
|
||||
|
||||
if (args.length > argIndex)
|
||||
bitcoinFee = Coin.parseCoin(args[argIndex++]);
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
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:");
|
||||
|
||||
System.out.println(String.format("Refund Bitcoin address: %s", redeemBitcoinAddress));
|
||||
System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString()));
|
||||
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", refundBitcoinAddress));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
|
||||
Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
|
||||
|
||||
if (!derivedP2shAddress.equals(p2shAddress)) {
|
||||
System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
bitcoinAmount = bitcoinAmount.add(bitcoinFee);
|
||||
|
||||
long medianBlockTime = BTC.getInstance().getMedianBlockTime();
|
||||
System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (now < medianBlockTime * 1000L)
|
||||
System.out.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC)));
|
||||
|
||||
// Check P2SH is funded
|
||||
final long startTime = lockTime - 86400;
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
|
||||
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", p2shAddress, BTC.FORMAT.format(p2shBalance)));
|
||||
|
||||
// Grab all P2SH funding transactions (just in case there are more than one)
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime);
|
||||
if (fundingOutputs == null) {
|
||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't redeem 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);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("Repository issue: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
158
src/test/java/org/qora/test/btcacct/DeployAT.java
Normal file
158
src/test/java/org/qora/test/btcacct/DeployAT.java
Normal file
@ -0,0 +1,158 @@
|
||||
package org.qora.test.btcacct;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.security.Security;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qora.account.PrivateKeyAccount;
|
||||
import org.qora.asset.Asset;
|
||||
import org.qora.controller.Controller;
|
||||
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;
|
||||
import org.qora.group.Group;
|
||||
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 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;
|
||||
|
||||
public class DeployAT {
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <redeem Qortal address> <HASH160-of-secret> <locktime> (<initial QORT payout>)"));
|
||||
System.err.println(String.format("example: DeployAT "
|
||||
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
|
||||
+ "\t3.1415 \\\n"
|
||||
+ "\tQgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v \\\n"
|
||||
+ "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n"
|
||||
+ "\t1585920000 \\\n"
|
||||
+ "\t0.0001"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 5 || args.length > 6)
|
||||
usage(null);
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Settings.fileInstance("settings-test.json");
|
||||
|
||||
byte[] refundPrivateKey = null;
|
||||
BigDecimal qortAmount = null;
|
||||
String redeemAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
BigDecimal initialPayout = BigDecimal.ZERO.setScale(8);
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
refundPrivateKey = Base58.decode(args[argIndex++]);
|
||||
if (refundPrivateKey.length != 32)
|
||||
usage("Refund private key must be 32 bytes");
|
||||
|
||||
qortAmount = new BigDecimal(args[argIndex++]);
|
||||
if (qortAmount.signum() <= 0)
|
||||
usage("QORT amount must be positive");
|
||||
|
||||
redeemAddress = args[argIndex++];
|
||||
if (!Crypto.isValidAddress(redeemAddress))
|
||||
usage("Redeem address invalid");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("Hash of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
|
||||
if (args.length > argIndex)
|
||||
initialPayout = new BigDecimal(args[argIndex++]).setScale(8);
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
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:");
|
||||
|
||||
PrivateKeyAccount refundAccount = new PrivateKeyAccount(repository, refundPrivateKey);
|
||||
System.out.println(String.format("Refund Qortal address: %s", refundAccount.getAddress()));
|
||||
|
||||
System.out.println(String.format("QORT amount (INCLUDING FEES): %s", qortAmount.toPlainString()));
|
||||
|
||||
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
System.out.println(String.format("Redeem Qortal address: %s", redeemAddress));
|
||||
|
||||
// New/derived info
|
||||
|
||||
System.out.println("\nCHECKING info from other party:");
|
||||
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()), lockTime));
|
||||
System.out.println("Make sure this is BEFORE P2SH lockTime to allow you to refund AT before P2SH refunded");
|
||||
|
||||
// Deploy AT
|
||||
final int BLOCK_TIME = 60; // seconds
|
||||
final int refundTimeout = (lockTime - (int) (System.currentTimeMillis() / 1000L)) / BLOCK_TIME;
|
||||
|
||||
byte[] creationBytes = BTCACCT.buildQortalAT(secretHash, redeemAddress, refundTimeout, initialPayout);
|
||||
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
|
||||
long txTimestamp = System.currentTimeMillis();
|
||||
byte[] lastReference = refundAccount.getLastReference();
|
||||
|
||||
if (lastReference == null) {
|
||||
System.err.println(String.format("Qortal account %s has no last reference", refundAccount.getAddress()));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
BigDecimal fee = BigDecimal.ZERO;
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, refundAccount.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, "QORT-BTC", "QORT-BTC ACCT", "", "", creationBytes, qortAmount, Asset.QORT);
|
||||
|
||||
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
deployAtTransaction.sign(refundAccount);
|
||||
|
||||
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(String.format("Number format exception: %s", e.getMessage()));
|
||||
} catch (DataException e) {
|
||||
throw new RuntimeException("Repository issue: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -121,7 +121,7 @@ public class Initiate {
|
||||
int lockTime = (int) ((System.currentTimeMillis() / 1000L) + REFUND_TIMEOUT);
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(tradeKey.getPubKeyHash(), yourBitcoinAddress.getHash(), theirBitcoinAddress.getHash(), lockTime);
|
||||
byte[] redeemScriptBytes = null; // BTCACCT.buildScript(tradeKey.getPubKeyHash(), yourBitcoinAddress.getHash(), theirBitcoinAddress.getHash(), lockTime);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
|
||||
|
@ -4,18 +4,16 @@ import java.security.Security;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.AddressFormatException;
|
||||
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.RegTestParams;
|
||||
import org.bitcoinj.params.TestNet3Params;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qora.controller.Controller;
|
||||
@ -30,25 +28,6 @@ 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 trade private key to Responder.
|
||||
* Responder uses their public key + tx signature + trade pubkey + script as input to BTC P2SH address, releasing BTC amount 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 Redeem {
|
||||
|
||||
static {
|
||||
@ -56,16 +35,17 @@ public class Redeem {
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
}
|
||||
|
||||
private static final long REFUND_TIMEOUT = 600L; // seconds
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: Redeem <your-BTC-pubkey> <their-BTC-P2PKH> <trade-PRIVATE-key> <locktime> <P2SH-address> (<BTC-redeem/refund-fee>)"));
|
||||
System.err.println(String.format("example: Redeem 032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n"
|
||||
System.err.println(String.format("usage: Redeem <P2SH-address> <refund-BTC-P2PKH> <redeem-BTC-PRIVATE-key> <secret> <locktime> (<BTC-redeem/refund-fee>)"));
|
||||
System.err.println(String.format("example: Redeem "
|
||||
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
|
||||
+ "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n"
|
||||
+ "\teb95e1c1a5e9e6733549faec85b71f74f67638ea63b0acf2f077e9d0cb94dfe8 1575653814 2Mtn4aLjjWVEWckdoTMK7P8WbkXJf1ES6yL"));
|
||||
+ "\tec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03 \\\n"
|
||||
+ "\t736563726574 \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
@ -75,39 +55,44 @@ public class Redeem {
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Settings.fileInstance("settings-test.json");
|
||||
NetworkParameters params = RegTestParams.get();
|
||||
// TestNet3Params.get();
|
||||
|
||||
ECKey yourBitcoinKey = null;
|
||||
Address theirBitcoinAddress = null;
|
||||
byte[] tradePrivateKey = null;
|
||||
int lockTime = 0;
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
Address p2shAddress = null;
|
||||
Address refundBitcoinAddress = null;
|
||||
byte[] redeemPrivateKey = null;
|
||||
byte[] secret = null;
|
||||
int lockTime = 0;
|
||||
Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
int argIndex = 0;
|
||||
|
||||
yourBitcoinKey = ECKey.fromPublicOnly(HashCode.fromString(args[argIndex++]).asBytes());
|
||||
|
||||
theirBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (theirBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Their BTC address is not in P2PKH form");
|
||||
|
||||
tradePrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (tradePrivateKey.length != 32)
|
||||
usage("Trade private key not 32 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
|
||||
p2shAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
|
||||
usage("P2SH address invalid");
|
||||
|
||||
refundBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Refund BTC address must be in P2PKH form");
|
||||
|
||||
redeemPrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
// Auto-trim
|
||||
if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38)
|
||||
redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33);
|
||||
if (redeemPrivateKey.length != 32)
|
||||
usage("Redeem private key must be 32 bytes");
|
||||
|
||||
secret = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secret.length == 0)
|
||||
usage("Invalid secret bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
|
||||
if (args.length > argIndex)
|
||||
bitcoinFee = Coin.parseCoin(args[argIndex++]);
|
||||
} catch (NumberFormatException | AddressFormatException e) {
|
||||
usage(String.format("Argument format exception: %s", e.getMessage()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
try {
|
||||
@ -120,21 +105,22 @@ public class Redeem {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
System.out.println("Confirm the following is correct based on the info you've given:");
|
||||
|
||||
System.out.println(String.format("Your Bitcoin address: %s", Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH)));
|
||||
System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress));
|
||||
System.out.println(String.format("Trade PRIVATE key: %s", HashCode.fromBytes(tradePrivateKey)));
|
||||
System.out.println(String.format("Redeem PRIVATE key: %s", HashCode.fromBytes(redeemPrivateKey)));
|
||||
System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
System.out.println(String.format("Bitcoin redeem fee: %s", bitcoinFee.toPlainString()));
|
||||
|
||||
// New/derived info
|
||||
|
||||
System.out.println("\nCHECKING info from other party:");
|
||||
byte[] secretHash = BTC.hash160(secret);
|
||||
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
|
||||
|
||||
ECKey tradeKey = ECKey.fromPrivate(tradePrivateKey);
|
||||
System.out.println(String.format("Trade pubkeyhash: %s", HashCode.fromBytes(tradeKey.getPubKeyHash())));
|
||||
ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey);
|
||||
Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH);
|
||||
System.out.println(String.format("Redeem recipient (PKH): %s (%s)", redeemAddress, HashCode.fromBytes(redeemAddress.getHash())));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(tradeKey.getPubKeyHash(), theirBitcoinAddress.getHash(), yourBitcoinKey.getPubKeyHash(), lockTime);
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
|
||||
@ -159,7 +145,10 @@ public class Redeem {
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), lockTime - REFUND_TIMEOUT);
|
||||
// Check P2SH is funded
|
||||
final long startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400;
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
|
||||
if (p2shBalance == null) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
@ -167,8 +156,16 @@ public class Redeem {
|
||||
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().getOutputs(p2shAddress.toString(), lockTime - REFUND_TIMEOUT);
|
||||
System.out.println(String.format("Found %d unspent output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime);
|
||||
if (fundingOutputs == null) {
|
||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't redeem spent/unfunded P2SH"));
|
||||
@ -184,7 +181,9 @@ public class Redeem {
|
||||
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
|
||||
|
||||
Coin redeemAmount = p2shBalance.subtract(bitcoinFee);
|
||||
Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, tradeKey, yourBitcoinKey.getPubKey(), fundingOutput, redeemScriptBytes);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(redeemAmount), BTC.FORMAT.format(bitcoinFee)));
|
||||
|
||||
Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, secret);
|
||||
|
||||
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
||||
|
||||
|
@ -4,18 +4,16 @@ import java.security.Security;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.AddressFormatException;
|
||||
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.RegTestParams;
|
||||
import org.bitcoinj.params.TestNet3Params;
|
||||
import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.qora.controller.Controller;
|
||||
@ -55,16 +53,17 @@ public class Refund {
|
||||
System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
|
||||
}
|
||||
|
||||
private static final long REFUND_TIMEOUT = 600L; // seconds
|
||||
|
||||
private static void usage(String error) {
|
||||
if (error != null)
|
||||
System.err.println(error);
|
||||
|
||||
System.err.println(String.format("usage: Refund <your-BTC-pubkey> <their-BTC-P2PKH> <trade-PRIVATE-key> <locktime> <P2SH-address> (<BTC-redeem/refund-fee>)"));
|
||||
System.err.println(String.format("example: Refund 03aa20871c2195361f2826c7a649eab6b42639630c4d8c33c55311d5c1e476b5d6 \\\n"
|
||||
System.err.println(String.format("usage: Refund <P2SH-address> <refund-BTC-PRIVATE-KEY> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)"));
|
||||
System.err.println(String.format("example: Refund "
|
||||
+ "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n"
|
||||
+ "\tef027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c01b576fb7e \\\n"
|
||||
+ "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n"
|
||||
+ "\teb95e1c1a5e9e6733549faec85b71f74f67638ea63b0acf2f077e9d0cb94dfe8 1575653814 2Mtn4aLjjWVEWckdoTMK7P8WbkXJf1ES6yL"));
|
||||
+ "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n"
|
||||
+ "\t1585920000"));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
@ -74,39 +73,44 @@ public class Refund {
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 0);
|
||||
Settings.fileInstance("settings-test.json");
|
||||
NetworkParameters params = RegTestParams.get();
|
||||
// TestNet3Params.get();
|
||||
|
||||
ECKey yourBitcoinKey = null;
|
||||
Address theirBitcoinAddress = null;
|
||||
byte[] tradePrivateKey = null;
|
||||
int lockTime = 0;
|
||||
BTC btc = BTC.getInstance();
|
||||
NetworkParameters params = btc.getNetworkParameters();
|
||||
|
||||
Address p2shAddress = null;
|
||||
byte[] refundPrivateKey = null;
|
||||
Address redeemBitcoinAddress = null;
|
||||
byte[] secretHash = null;
|
||||
int lockTime = 0;
|
||||
Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
|
||||
|
||||
int argIndex = 0;
|
||||
try {
|
||||
int argIndex = 0;
|
||||
|
||||
yourBitcoinKey = ECKey.fromPublicOnly(HashCode.fromString(args[argIndex++]).asBytes());
|
||||
|
||||
theirBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (theirBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Their BTC address is not in P2PKH form");
|
||||
|
||||
tradePrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (tradePrivateKey.length != 32)
|
||||
usage("Trade private key not 32 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
|
||||
p2shAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (p2shAddress.getOutputScriptType() != ScriptType.P2SH)
|
||||
usage("P2SH address invalid");
|
||||
|
||||
refundPrivateKey = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
// Auto-trim
|
||||
if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38)
|
||||
refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33);
|
||||
if (refundPrivateKey.length != 32)
|
||||
usage("Refund private key must be 32 bytes");
|
||||
|
||||
redeemBitcoinAddress = Address.fromString(params, args[argIndex++]);
|
||||
if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH)
|
||||
usage("Their BTC address must be in P2PKH form");
|
||||
|
||||
secretHash = HashCode.fromString(args[argIndex++]).asBytes();
|
||||
if (secretHash.length != 20)
|
||||
usage("HASH160 of secret must be 20 bytes");
|
||||
|
||||
lockTime = Integer.parseInt(args[argIndex++]);
|
||||
|
||||
if (args.length > argIndex)
|
||||
bitcoinFee = Coin.parseCoin(args[argIndex++]);
|
||||
} catch (NumberFormatException | AddressFormatException e) {
|
||||
usage(String.format("Argument format exception: %s", e.getMessage()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage()));
|
||||
}
|
||||
|
||||
try {
|
||||
@ -119,21 +123,21 @@ public class Refund {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
System.out.println("Confirm the following is correct based on the info you've given:");
|
||||
|
||||
System.out.println(String.format("Your Bitcoin address: %s", Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH)));
|
||||
System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress));
|
||||
System.out.println(String.format("Trade PRIVATE key: %s", HashCode.fromBytes(tradePrivateKey)));
|
||||
System.out.println(String.format("Refund PRIVATE key: %s", HashCode.fromBytes(refundPrivateKey)));
|
||||
System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress));
|
||||
System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime));
|
||||
System.out.println(String.format("P2SH address: %s", p2shAddress));
|
||||
System.out.println(String.format("Bitcoin redeem fee: %s", bitcoinFee.toPlainString()));
|
||||
System.out.println(String.format("Refund miner's fee: %s", BTC.FORMAT.format(bitcoinFee)));
|
||||
|
||||
// New/derived info
|
||||
|
||||
System.out.println("\nCHECKING info from other party:");
|
||||
|
||||
ECKey tradeKey = ECKey.fromPrivate(tradePrivateKey);
|
||||
System.out.println(String.format("Trade pubkeyhash: %s", HashCode.fromBytes(tradeKey.getPubKeyHash())));
|
||||
ECKey refundKey = ECKey.fromPrivate(refundPrivateKey);
|
||||
Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH);
|
||||
System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash())));
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(tradeKey.getPubKeyHash(), yourBitcoinKey.getPubKeyHash(), theirBitcoinAddress.getHash(), lockTime);
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
|
||||
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
|
||||
|
||||
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
|
||||
@ -163,7 +167,10 @@ public class Refund {
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), lockTime - REFUND_TIMEOUT);
|
||||
// Check P2SH is funded
|
||||
final long startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400;
|
||||
|
||||
Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime);
|
||||
if (p2shBalance == null) {
|
||||
System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress));
|
||||
System.exit(2);
|
||||
@ -171,8 +178,16 @@ public class Refund {
|
||||
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().getOutputs(p2shAddress.toString(), lockTime - REFUND_TIMEOUT);
|
||||
System.out.println(String.format("Found %d unspent output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
List<TransactionOutput> fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime);
|
||||
if (fundingOutputs == null) {
|
||||
System.err.println(String.format("Can't find outputs for P2SH"));
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : "")));
|
||||
|
||||
for (TransactionOutput fundingOutput : fundingOutputs)
|
||||
System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue())));
|
||||
|
||||
if (fundingOutputs.isEmpty()) {
|
||||
System.err.println(String.format("Can't refund spent/unfunded P2SH"));
|
||||
@ -188,11 +203,13 @@ public class Refund {
|
||||
System.out.println(String.format("Using output %s:%d for refund", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
|
||||
|
||||
Coin refundAmount = p2shBalance.subtract(bitcoinFee);
|
||||
Transaction refundTransaction = BTCACCT.buildRefundTransaction(refundAmount, tradeKey, yourBitcoinKey.getPubKey(), fundingOutput, redeemScriptBytes, lockTime);
|
||||
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(refundAmount), BTC.FORMAT.format(bitcoinFee)));
|
||||
|
||||
byte[] refundBytes = refundTransaction.bitcoinSerialize();
|
||||
Transaction redeemTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, lockTime);
|
||||
|
||||
System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(refundBytes).toString()));
|
||||
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();
|
||||
|
||||
System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString()));
|
||||
} catch (NumberFormatException e) {
|
||||
usage(String.format("Number format exception: %s", e.getMessage()));
|
||||
} catch (DataException e) {
|
||||
|
@ -136,7 +136,7 @@ public class Respond2 {
|
||||
byte[] secretHash = HashCode.fromString(secretHashHex).asBytes();
|
||||
System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString());
|
||||
|
||||
byte[] redeemScriptBytes = BTCACCT.buildScript(secretHash, theirBitcoinPubKey, yourBitcoinPubKey, lockTime);
|
||||
byte[] redeemScriptBytes = null; // BTCACCT.buildScript(secretHash, theirBitcoinPubKey, yourBitcoinPubKey, lockTime);
|
||||
System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString());
|
||||
|
||||
byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);
|
||||
@ -166,7 +166,7 @@ public class Respond2 {
|
||||
System.out.println("\nYour response:");
|
||||
|
||||
// If good, deploy AT
|
||||
byte[] creationBytes = BTCACCT.buildCiyamAT(secretHash, theirQortPubKey, REFUND_TIMEOUT / 60);
|
||||
byte[] creationBytes = null; // BTCACCT.buildQortalAT(secretHash, theirQortPubKey, REFUND_TIMEOUT / 60);
|
||||
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
|
||||
|
||||
BigDecimal qortAmount = new BigDecimal(rawQortAmount).setScale(8);
|
||||
|
Loading…
x
Reference in New Issue
Block a user