mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-22 20:26: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:
@@ -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() {
|
||||
|
Reference in New Issue
Block a user